摘要
本文详细解析了一款基于Pymunk物理引擎的2D坦克对战游戏的完整开发过程。游戏采用Python的Pygame库进行图形渲染,结合Pymunk物理引擎实现真实的物理模拟。文章将从游戏创意、技术选型、架构设计、核心算法、代码实现等多个维度,深入剖析这款游戏的技术实现细节,为物理游戏开发提供全面的技术参考。
关键词
物理引擎、Pymunk、Pygame、2D游戏、物理模拟、游戏开发、Python、碰撞检测
目录
-
游戏创意与设计理念
-
技术选型:为什么选择Pymunk+Pygame
-
游戏架构设计
-
物理引擎核心概念解析
-
游戏对象模型设计
-
碰撞检测与响应系统
-
粒子系统与视觉效果
-
用户界面与交互设计
-
游戏状态管理与控制流程
-
性能优化与调试技巧
-
关键代码实现详解
-
开发经验总结与展望
1. 游戏创意与设计理念
1.1 游戏核心玩法
本游戏的核心玩法是回合制2D坦克对战,灵感来源于经典的"百战天虫"和"坦克大战"系列。游戏采用物理模拟为基础,玩家需要控制坦克在复杂地形中移动、瞄准,并发射不同类型的炮弹攻击对手。游戏的核心乐趣在于:
-
物理模拟的真实性:炮弹飞行轨迹受重力、风力、地形碰撞等因素影响
-
策略多样性:玩家需要考虑角度、力量、炮弹类型、地形影响等多重因素
-
环境互动:地形可被破坏,爆炸会产生冲击波,影响周围物体
1.2 设计目标
-
物理准确性:确保所有物理交互符合真实物理规律
-
视觉效果:实现流畅的动画、粒子效果和爆炸特效
-
操作友好:提供直观的瞄准辅助和清晰的用户界面
-
可扩展性:设计模块化架构,便于添加新功能
2. 技术选型:为什么选择Pymunk+Pygame
2.1 Pygame的优势
Pygame是一个成熟的2D游戏开发库,具有以下优势:
python
# Pygame基础架构示例
import pygame
# 初始化
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
# 主循环
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 游戏逻辑
# 绘制
screen.fill((0, 0, 0))
# ... 绘制代码
pygame.display.flip()
clock.tick(60)
pygame.quit()
Pygame特点:
-
简单易用的2D图形API
-
内置事件处理系统
-
音频、输入设备支持
-
活跃的社区和丰富的资源
2.2 Pymunk物理引擎
Pymunk是Chipmunk物理引擎的Python绑定,专为2D物理模拟设计:
python
import pymunk
# 创建物理空间
space = pymunk.Space()
space.gravity = (0, 981) # 重力加速度,向下981像素/秒²
# 创建刚体
body = pymunk.Body(mass=1, moment=10)
body.position = (100, 100)
# 创建形状
circle = pymunk.Circle(body, radius=10)
circle.elasticity = 0.8 # 弹性系数
circle.friction = 0.5 # 摩擦系数
# 添加到空间
space.add(body, circle)
Pymunk核心优势:
-
稳定的刚体物理模拟
-
精确的碰撞检测
-
关节、约束支持
-
良好的性能表现
2.3 技术栈组合优势
Pymunk和Pygame的组合形成了完美的技术栈:
-
职责分离:Pymunk处理物理模拟,Pygame处理渲染
-
数据同步:物理引擎更新对象位置,图形引擎负责绘制
-
性能平衡:物理计算与渲染分离,优化性能
3. 游戏架构设计
3.1 整体架构
游戏采用经典的游戏循环架构,结合面向对象设计模式:
游戏主循环
├── 事件处理层
├── 物理更新层
├── 游戏逻辑层
├── 渲染绘制层
└── 用户界面层
3.2 核心类设计
python
# 类关系示意
class UltimateTankBattle: # 游戏主控制器
├── GameState # 游戏状态枚举
├── ProjectileType # 炮弹类型枚举
├── TerrainType # 地形类型枚举
├── tanks: List[Tank] # 坦克对象列表
├── projectiles: List[Projectile] # 炮弹列表
├── explosions: List[Explosion] # 爆炸效果列表
└── particles: List[dict] # 粒子系统
class Tank: # 坦克类
├── body: pymunk.Body # 物理刚体
├── shape: pymunk.Shape # 碰撞形状
└── barrel_angle: float # 炮管角度
class Projectile: # 炮弹类
├── body: pymunk.Body
├── shape: pymunk.Shape
└── projectile_type: ProjectileType
class Explosion: # 爆炸效果类
├── radius: float
├── power: float
└── alpha: int # 透明度
3.3 数据流设计
bash
用户输入 → 事件处理器 → 游戏状态更新 → 物理引擎更新 → 渲染输出
↓ ↓ ↓ ↓ ↓
键盘/鼠标 → 状态转换 → 对象位置更新 → 碰撞检测 → 屏幕绘制
4. 物理引擎核心概念解析
4.1 物理空间(Space)
物理空间是Pymunk的核心容器,管理所有物理对象:
python
class UltimateTankBattle:
def __init__(self):
# 创建物理空间
self.space = pymunk.Space()
# 设置重力 - 向下800像素/秒²
self.space.gravity = (0, 800)
# 设置迭代次数,影响物理模拟精度
self.space.iterations = 10
# 启用多线程(如果可用)
self.space.threaded = True
关键参数说明:
-
gravity: 重力向量,控制下落加速度 -
iterations: 迭代次数,值越高模拟越精确但性能开销越大 -
damping: 阻尼系数,模拟空气阻力 -
threaded: 多线程支持,提高复杂场景性能
4.2 刚体(Body)
刚体是物理模拟的基本单位,表示具有质量、位置、速度的物体:
python
class Tank:
def __init__(self, x, y, player_id, color, facing_right=True):
# 坦克物理参数
mass = 150 # 质量,影响惯性和受外力响应
width, height = 60, 40 # 坦克尺寸
# 计算转动惯量
moment = pymunk.moment_for_box(mass, (width, height))
# 创建动态刚体
self.body = pymunk.Body(mass, moment)
self.body.position = (x, y) # 初始位置
# 设置速度限制
self.body.velocity_limit = 500
self.body.angular_velocity_limit = 3
# 设置阻尼,模拟地面摩擦
self.body.damping = 0.9
刚体类型:
-
动态刚体:有质量,受力和碰撞影响
-
静态刚体:质量无限大,不受力影响,用于地形
-
运动学刚体:可通过代码控制位置,不受力但可影响其他物体
4.3 形状(Shape)
形状定义了刚体的碰撞边界,附加在刚体上:
python
class Tank:
def __init__(self, x, y, player_id, color, facing_right=True):
# 创建矩形碰撞形状
self.shape = pymunk.Poly.create_box(self.body, (width, height))
# 物理属性设置
self.shape.friction = 1.2 # 摩擦系数,影响滑动
self.shape.elasticity = 0.2 # 弹性系数,0=完全非弹性,1=完全弹性
self.shape.density = 1.0 # 密度,与质量相关
# 设置碰撞类型
self.shape.collision_type = 1
# 设置碰撞掩码
self.shape.filter = pymunk.ShapeFilter(categories=0b1, mask=0b1110)
形状类型:
-
Circle: 圆形 -
Segment: 线段 -
Poly: 多边形 -
BB: 轴对齐包围盒
4.4 碰撞检测系统
Pymunk提供多层级的碰撞检测:
python
class Projectile:
def check_collision_immediate(self, tanks, terrain_points, obstacles):
"""立即碰撞检测 - 一碰就炸"""
x, y = self.body.position.x, self.body.position.y
# 1. 屏幕边界检测
if x < -100 or x > 1500 or y < -100:
return True
# 2. 地形碰撞检测
for i in range(len(terrain_points) - 1):
x1, y1 = terrain_points[i]
x2, y2 = terrain_points[i + 1]
if x1 <= x <= x2:
t = (x - x1) / (x2 - x1) if x2 != x1 else 0
terrain_y = y1 + (y2 - y1) * t
if y >= terrain_y - 5: # 容差5像素
return True
# 3. 障碍物碰撞检测
for obstacle in obstacles:
shape = obstacle["shape"]
if hasattr(shape, 'bb'):
bb = shape.bb
if bb.left <= x <= bb.right and bb.bottom <= y <= bb.top:
return True
# 4. 坦克碰撞检测
for tank in tanks:
# 创建坦克的碰撞矩形
tank_rect = pygame.Rect(
tank.body.position.x - 30,
tank.body.position.y - 20,
60, 40
)
if tank_rect.collidepoint(x, y):
return True
return False
碰撞检测策略:
-
空间划分:通过四叉树优化碰撞检测
-
层级检测:先粗检测后精检测
-
碰撞过滤:通过掩码避免不必要的碰撞检测
5. 游戏对象模型设计
5.1 坦克模型设计
坦克是游戏的核心交互对象,设计考虑:
python
class Tank:
def __init__(self, x, y, player_id, color, facing_right=True):
# 游戏逻辑属性
self.player_id = player_id
self.color = color
self.health = 100
self.max_health = 100
self.power = 60.0
# 物理属性
self.barrel_length = 30
self.facing_right = facing_right
# 炮管角度范围限制
if facing_right:
# 面向右侧的坦克
self.barrel_angle_min = -math.pi/2
self.barrel_angle_max = 0
self.barrel_angle = -math.pi/4
else:
# 面向左侧的坦克
self.barrel_angle_min = math.pi
self.barrel_angle_max = 3*math.pi/2
self.barrel_angle = 5*math.pi/4
# 创建物理刚体
self.create_physics_body(x, y)
def create_physics_body(self, x, y):
"""创建坦克物理模型"""
mass = 150
width, height = 60, 40
# 计算转动惯量
moment = pymunk.moment_for_box(mass, (width, height))
# 创建刚体
self.body = pymunk.Body(mass, moment)
self.body.position = (x, y)
# 创建碰撞形状
self.shape = pymunk.Poly.create_box(self.body, (width, height))
self.shape.friction = 1.2
self.shape.elasticity = 0.2
坦克控制方法:
python
class Tank:
def aim_at(self, target_x, target_y):
"""瞄准目标位置"""
dx = target_x - self.body.position.x
dy = target_y - self.body.position.y
# 计算角度
angle = math.atan2(dy, dx)
# 角度限制
if self.facing_right:
angle = max(self.barrel_angle_min, min(self.barrel_angle_max, angle))
else:
if angle < 0:
angle += 2*math.pi
if angle < math.pi:
angle = math.pi
elif angle > 3*math.pi/2:
angle = 3*math.pi/2
self.barrel_angle = angle
def move(self, force):
"""移动坦克"""
if not self.facing_right:
force = -force
# 应用力
self.body.apply_force_at_local_point((force, 0), (0, 0))
# 速度限制
max_speed = 200
if self.body.velocity.length > max_speed:
self.body.velocity = self.body.velocity.normalized() * max_speed
5.2 炮弹模型设计
炮弹是游戏的主要攻击手段,设计考虑不同类型的物理特性:
python
class Projectile:
def __init__(self, x, y, angle, power, projectile_type, space):
self.projectile_type = projectile_type
self.exploded = False
self.lifetime = 6.0
self.active = True
# 获取炮弹参数
params = projectile_type.value
radius = params["radius"]
mass = params["mass"]
# 创建物理刚体
moment = pymunk.moment_for_circle(mass, 0, radius)
self.body = pymunk.Body(mass, moment)
self.body.position = (x, y)
# 计算初始速度
velocity = power * 6
self.body.velocity = (
velocity * math.cos(angle),
velocity * math.sin(angle)
)
# 创建碰撞形状
self.shape = pymunk.Circle(self.body, radius)
self.shape.elasticity = 0.6
self.shape.friction = 0.4
self.shape.color = params["color"]
self.shape.collision_type = 2 # 设置碰撞类型
# 添加到空间
space.add(self.body, self.shape)
炮弹类型设计:
python
class ProjectileType(Enum):
"""炮弹类型枚举"""
STANDARD = {
"name": "标准弹",
"mass": 1.5, # 质量较轻
"radius": 8, # 半径较小
"color": (220, 220, 220),
"power": 100, # 基础威力
"explosion_radius": 50, # 爆炸半径
"trail_density": 0.
"trail_density": 0.6
}
HEAVY = {
"name": "重型弹",
"mass": 8.0, # 质量大,惯性大
"radius": 12, # 半径大
"color": (120, 120, 200),
"power": 200, # 威力中等
"explosion_radius": 80, # 爆炸范围中等
"trail_density": 0.8
}
EXPLOSIVE = {
"name": "高爆弹",
"mass": 2.0, # 质量轻
"radius": 10, # 半径中等
"color": (255, 120, 120),
"power": 300, # 威力最大
"explosion_radius": 100, # 爆炸范围最大
"trail_density": 1.0
}
炮弹类型设计策略:
-
差异化设计:不同类型的炮弹在质量、速度、威力、爆炸范围上形成差异
-
风险收益平衡:高爆弹威力大但质量轻,受风力影响更大
-
视觉区分:通过颜色和大小提供直观的视觉反馈
5.3 爆炸系统设计
爆炸是游戏的核心视觉和物理效果:
python
class Explosion:
def __init__(self, x, y, radius, power, color):
# 爆炸参数
self.x, self.y = x, y
self.base_radius = radius
self.power = power
self.color = color
# 视觉效果参数
self.radius = 10
self.max_radius = radius * 2.0
self.growth_rate = radius * 5
self.alpha = 255
self.fade_rate = 10
self.frame = 0
def update(self, dt):
"""更新爆炸效果"""
if self.alpha <= 0:
return False
# 扩张效果
if self.radius < self.max_radius:
self.radius += self.growth_rate * dt
# 淡出效果
self.alpha -= self.fade_rate
self.frame += 1
return True
def draw(self, screen):
"""绘制爆炸效果"""
if self.alpha <= 0:
return
# 创建爆炸表面
surface_size = int(self.radius * 2.5)
explosion_surface = pygame.Surface((surface_size, surface_size), pygame.SRCALPHA)
center = surface_size // 2
# 脉动效果
pulse = math.sin(self.frame * 0.2) * 0.1 + 1.0
current_radius = self.radius * pulse
# 三层爆炸效果
# 1. 外层光晕
outer_radius = int(current_radius * 1.3)
outer_color = (255, 200, 50, int(self.alpha * 0.4))
pygame.draw.circle(explosion_surface, outer_color,
(center, center), outer_radius)
# 2. 中层火焰
mid_radius = int(current_radius)
mid_color = (255, 150, 30, int(self.alpha * 0.6))
pygame.draw.circle(explosion_surface, mid_color,
(center, center), mid_radius)
# 3. 核心高温区
core_radius = int(current_radius * 0.6)
core_color = (255, 100, 20, int(self.alpha * 0.8))
pygame.draw.circle(explosion_surface, core_color,
(center, center), core_radius)
# 绘制到屏幕
screen.blit(explosion_surface,
(int(self.x - current_radius), int(self.y - current_radius)))
爆炸物理效果:
python
class UltimateTankBattle:
def create_explosion(self, x, y, radius, power, color):
"""创建爆炸效果"""
explosion = Explosion(x, y, radius, power, color)
self.explosions.append(explosion)
# 创建粒子效果
self.create_explosion_particles(x, y, radius)
# 对周围物体施加物理影响
self.apply_explosion_force(x, y, radius, power)
# 造成伤害
self.apply_explosion_damage(x, y, radius, power)
6. 碰撞检测与响应系统
6.1 多层碰撞检测架构
游戏采用多层碰撞检测架构,确保效率和准确性:
python
class UltimateTankBattle:
def check_all_collisions(self):
"""执行所有碰撞检测"""
# 1. 空间划分优化检测
self.space.reindex_shapes_for_body(self.space.static_body)
# 2. 执行物理引擎碰撞检测
self.space.step(1/60)
# 3. 自定义精确检测
for projectile in self.projectiles:
if self.check_projectile_collisions(projectile):
self.handle_collision_response(projectile)
def check_projectile_collisions(self, projectile):
"""检测炮弹碰撞"""
x, y = projectile.body.position
# 快速拒绝测试
if not self.is_in_bounds(x, y):
return "out_of_bounds"
# 地形碰撞检测
if self.check_terrain_collision(x, y):
return "terrain"
# 障碍物碰撞检测
if self.check_obstacle_collision(x, y):
return "obstacle"
# 坦克碰撞检测
tank_hit = self.check_tank_collision(x, y)
if tank_hit:
return f"tank_{tank_hit.player_id}"
return None
6.2 碰撞响应处理
不同碰撞类型需要不同的响应处理:
python
class UltimateTankBattle:
def handle_collision_response(self, projectile, collision_type):
"""处理碰撞响应"""
if not projectile.active:
return
# 标记炮弹爆炸
projectile.exploded = True
projectile.active = False
# 根据碰撞类型处理
if collision_type == "terrain":
self.handle_terrain_collision(projectile)
elif collision_type.startswith("tank_"):
player_id = int(collision_type.split("_")[1])
self.handle_tank_hit(projectile, player_id)
elif collision_type == "obstacle":
self.handle_obstacle_collision(projectile)
# 创建爆炸效果
self.create_explosion(
projectile.body.position.x,
projectile.body.position.y,
projectile.projectile_type.value["explosion_radius"],
projectile.projectile_type.value["power"],
projectile.projectile_type.value["color"]
)
def handle_terrain_collision(self, projectile):
"""处理地形碰撞"""
x, y = projectile.body.position
# 在地形上创建弹坑
self.create_crater(x, y,
projectile.projectile_type.value["explosion_radius"] * 0.5)
# 生成地形碎片
self.create_terrain_debris(x, y,
projectile.projectile_type.value["power"])
def handle_tank_hit(self, projectile, player_id):
"""处理坦克命中"""
tank = self.players[player_id]["tank"]
if not tank:
return
# 计算伤害
dx = tank.body.position.x - projectile.body.position.x
dy = tank.body.position.y - projectile.body.position.y
distance = math.sqrt(dx*dx + dy*dy)
# 距离越近伤害越高
explosion_radius = projectile.projectile_type.value["explosion_radius"]
damage_multiplier = 1.0 - (distance / (explosion_radius * 1.5))
damage = projectile.projectile_type.value["power"] * damage_multiplier
# 应用伤害
tank.take_damage(damage)
# 施加冲击力
if distance > 0:
force_magnitude = projectile.projectile_type.value["power"] * 3000 / (distance + 1)
force_x = (dx / distance) * force_magnitude
force_y = (dy / distance) * force_magnitude
tank.body.apply_impulse_at_local_point((force_x, force_y), (0, 0))
6.3 碰撞优化策略
为了提高碰撞检测效率,采用多种优化策略:
python
class UltimateTankBattle:
def optimize_collision_detection(self):
"""优化碰撞检测"""
# 1. 空间划分
self.setup_spatial_hash()
# 2. 碰撞过滤
self.setup_collision_filters()
# 3. 碰撞组管理
self.setup_collision_groups()
# 4. 碰撞回调
self.setup_collision_handlers()
def setup_spatial_hash(self):
"""设置空间哈希网格"""
# 创建空间哈希,将空间划分为网格
cell_size = 100 # 网格大小
self.space.use_spatial_hash(cell_size)
def setup_collision_filters(self):
"""设置碰撞过滤器"""
# 定义碰撞类别
CATEGORY_TERRAIN = 0b0001
CATEGORY_TANK = 0b0010
CATEGORY_PROJECTILE = 0b0100
CATEGORY_OBSTACLE = 0b1000
# 设置碰撞掩码
# 地形:只与炮弹碰撞
for segment in self.terrain_segments:
segment.filter = pymunk.ShapeFilter(
categories=CATEGORY_TERRAIN,
mask=CATEGORY_PROJECTILE
)
# 坦克:与所有物体碰撞
for tank in self.tanks:
tank.shape.filter = pymunk.ShapeFilter(
categories=CATEGORY_TANK,
mask=CATEGORY_TERRAIN | CATEGORY_PROJECTILE | CATEGORY_OBSTACLE
)
# 炮弹:与地形、坦克、障碍物碰撞
for projectile in self.projectiles:
projectile.shape.filter = pymunk.ShapeFilter(
categories=CATEGORY_PROJECTILE,
mask=CATEGORY_TERRAIN | CATEGORY_TANK | CATEGORY_OBSTACLE
)
7. 粒子系统与视觉效果
7.1 粒子系统架构
粒子系统是游戏视觉效果的核心:
python
class UltimateTankBattle:
def __init__(self):
# 粒子系统
self.particles = [] # 活动粒子列表
self.max_particles = 1000 # 最大粒子数
self.particle_pool = [] # 粒子对象池
# 预创建粒子对象池
self.init_particle_pool()
def init_particle_pool(self):
"""初始化粒子对象池"""
for _ in range(self.max_particles):
self.particle_pool.append({
"x": 0, "y": 0,
"vx": 0, "vy": 0,
"life": 0, "decay": 0,
"size": 0, "color": (0, 0, 0)
})
7.2 粒子类型与效果
python
class UltimateTankBattle:
def create_trail_particle(self, projectile):
"""创建炮弹尾迹粒子"""
params = projectile.projectile_type.value
speed = projectile.body.velocity.length
if speed > 5:
# 计算粒子位置(炮弹后方)
offset = random.uniform(-8, -2)
dx = projectile.body.velocity.x / max(speed, 1)
dy = projectile.body.velocity.y / max(speed, 1)
particle_x = projectile.body.position.x + dx * offset
particle_y = projectile.body.position.y + dy * offset
# 添加随机偏移
particle_x += random.uniform(-3, 3)
particle_y += random.uniform(-3, 3)
# 根据速度选择粒子类型
if speed > 30:
# 高速火焰粒子
color = COLORS["particle_fire"]
size = random.uniform(3, 7)
life = random.uniform(0.4, 0.8)
else:
# 低速烟雾粒子
color = COLORS["particle_smoke"]
size = random.uniform(2, 5)
life = random.uniform(0.3, 0.6)
# 从对象池获取粒子
particle = self.get_particle_from_pool()
if particle:
particle.update({
"x": particle_x, "y": particle_y,
"vx": projectile.body.velocity.x * 0.2 + random.uniform(-5, 5),
"vy": projectile.body.velocity.y * 0.2 + random.uniform(-5, 5),
"life": life,
"decay": random.uniform(0.01, 0.03),
"size": size,
"color": color
})
self.particles.append(particle)
def create_explosion_particles(self, x, y, radius):
"""创建爆炸粒子"""
num_particles = 60
for i in range(num_particles):
angle = random.uniform(0, 2*math.pi)
speed = random.uniform(2, 12)
# 火焰粒子(70%)
if i < num_particles * 0.7:
particle_color = (
random.randint(200, 255), # 红
random.randint(100, 200), # 绿
random.randint(0, 100) # 蓝
)
size = random.uniform(4, 9)
# 烟雾粒子(30%)
else:
particle_color = (
random.randint(100, 150),
random.randint(100, 150),
random.randint(100, 150)
)
size = random.uniform(3, 7)
# 从对象池获取粒子
particle = self.get_particle_from_pool()
if particle:
particle.update({
"x": x, "y": y,
"vx": math.cos(angle) * speed,
"vy": math.sin(angle) * speed,
"life": random.uniform(0.4, 1.2),
"decay": random.uniform(0.008, 0.02),
"size": size,
"color": particle_color
})
self.particles.append(particle)
7.3 粒子更新与渲染
python
class UltimateTankBattle:
def update_particle(self, particle, dt):
"""更新粒子状态"""
if particle["life"] <= 0:
return False
# 更新位置
particle["x"] += particle["vx"] * dt
particle["y"] += particle["vy"] * dt
# 更新生命周期
particle["life"] -= particle["decay"]
# 应用物理效果
particle["vy"] += 300 * dt # 重力
particle["vx"] *= 0.97 # 空气阻力
particle["vy"] *= 0.97
return particle["life"] > 0
def draw_particle(self, particle):
"""绘制单个粒子"""
if particle["life"] <= 0:
return
x, y = int(particle["x"]), int(particle["y"])
size = particle["size"]
color = particle["color"]
alpha = int(255 * particle["life"])
# 确保颜色有效
try:
if isinstance(color, (tuple, list)) and len(color) >= 3:
r = int(max(0, min(255, color[0])))
g = int(max(0, min(255, color[1])))
b = int(max(0, min(255, color[2])))
a = max(0, min(255, alpha))
rgba_color = (r, g, b, a)
else:
rgba_color = (200, 200, 200, alpha)
except (TypeError, ValueError, IndexError):
rgba_color = (200, 200, 200, alpha)
# 创建半透明表面
particle_surface = pygame.Surface((int(size*2), int(size*2)), pygame.SRCALPHA)
# 绘制粒子
if (isinstance(rgba_color, tuple) and len(rgba_color) == 4 and
all(isinstance(c, int) and 0 <= c <= 255 for c in rgba_color)):
pygame.draw.circle(particle_surface, rgba_color,
(int(size), int(size)), int(size))
else:
# 默认颜色
pygame.draw.circle(particle_surface, (255, 255, 255, alpha),
(int(size), int(size)), int(size))
# 绘制到屏幕
self.screen.blit(particle_surface, (x - int(size), y - int(size)))
8. 地形系统设计
8.1 程序化地形生成
python
class UltimateTankBattle:
def create_terrain(self, terrain_type):
"""创建程序化地形"""
self.terrain_points = []
num_points = 60
base_height = self.screen_height * 0.7
# 清除旧地形
for segment in self.terrain_segments:
self.space.remove(segment.body, segment)
self.terrain_segments.clear()
# 根据地形类型生成控制点
for i in range(num_points + 1):
x = (i / num_points) * self.screen_width
y = self.generate_terrain_height(x, terrain_type, base_height)
# 确保坦克位置平坦
if (abs(x - 200) < 80 or abs(x - 1200) < 80):
y = base_height
self.terrain_points.append((x, y))
# 平滑处理
self.smooth_terrain()
# 创建物理碰撞体
self.create_terrain_colliders()
def generate_terrain_height(self, x, terrain_type, base_height):
"""生成地形高度"""
if terrain_type == TerrainType.HILLS:
# 丘陵:多个正弦波叠加
noise = (math.sin(x * 0.003) * 120 +
math.sin(x * 0.006) * 60 +
math.sin(x * 0.012) * 30)
return base_height + noise
elif terrain_type == TerrainType.CANYON:
# 峡谷:中心凹陷
canyon_center = self.screen_width / 2
canyon_width = 300
canyon_depth = 100
distance = abs(x - canyon_center)
if distance < canyon_width:
depth_factor = 1 - (distance / canyon_width)
return base_height + canyon_depth * depth_factor
else:
return base_height + math.sin(x * 0.005) * 60
elif terrain_type == TerrainType.MOUNTAINS:
# 山脉:绝对值正弦波
noise = (abs(math.sin(x * 0.002)) * 180 +
abs(math.sin(x * 0.004)) * 90)
return base_height + noise
elif terrain_type == TerrainType.PLATEAU:
# 高原:中间平坦
plateau_center = self.screen_width / 2
plateau_width = 400
if abs(x - plateau_center) < plateau_width:
return base_height - 80
else:
return base_height + math.sin(x * 0.004) * 50
elif terrain_type == TerrainType.VALLEY:
# 山谷:中心凹陷
valley_center = self.screen_width / 2
valley_width = 350
valley_depth = 80
distance = abs(x - valley_center)
if distance < valley_width:
depth_factor = 1 - (distance / valley_width)
return base_height - valley_depth * depth_factor
else:
return base_height + math.sin(x * 0.004) * 40
elif terrain_type == TerrainType.ISLANDS:
# 岛屿:两个凸起
island1_center = self.screen_width * 0.3
island2_center = self.screen_width * 0.7
island_width = 200
island_height = 60
distance1 = abs(x - island1_center)
distance2 = abs(x - island2_center)
if distance1 < island_width:
height_factor = math.cos((distance1 / island_width) * math.pi/2)
return base_height - island_height * height_factor
elif distance2 < island_width:
height_factor = math.cos((distance2 / island_width) * math.pi/2)
return base_height - island_height * height_factor
else:
return base_height
else:
return base_height
def smooth_terrain(self):
"""平滑地形曲线"""
smoothed_points = []
for i in range(len(self.terrain_points) - 1):
x1, y1 = self.terrain_points[i]
x2, y2 = self.terrain_points[i + 1]
# 三次样条插值
for j in range(3):
t = j / 3
x = x1 + (x2 - x1) * t
y = y1 + (y2 - y1) * t
# 添加随机细节
if 0 < t < 1 and random.random() < 0.3:
y += random.uniform(-2, 2)
smoothed_points.append((x, y))
self.terrain_points = smoothed_points
def create_terrain_colliders(self):
"""创建地形物理碰撞体"""
for i in range(len(self.terrain_points) - 1):
# 创建静态刚体
body = pymunk.Body(body_type=pymunk.Body.STATIC)
# 创建线段形状
shape = pymunk.Segment(body,
self.terrain_points[i],
self.terrain_points[i + 1],
8) # 厚度8像素
# 设置物理属性
shape.elasticity = 0.4 # 中等弹性
shape.friction = 0.9 # 高摩擦,模拟泥土
shape.collision_type = 1 # 地形碰撞类型
# 添加到空间
self.space.add(body, shape)
self.terrain_segments.append(shape)
8.2 动态地形变形
地形在爆炸后应该产生弹坑效果,这需要实时更新地形几何:
python
class UltimateTankBattle:
def create_crater(self, x, y, radius):
"""在指定位置创建弹坑"""
new_terrain_points = []
for i in range(len(self.terrain_points)):
px, py = self.terrain_points[i]
dx = px - x
dy = py - y
distance = math.sqrt(dx*dx + dy*dy)
if distance < radius:
# 在爆炸范围内的点向下移动
depth = radius * math.cos((distance / radius) * (math.pi/2))
new_y = py + depth
new_terrain_points.append((px, new_y))
else:
new_terrain_points.append((px, py))
# 更新地形点
self.terrain_points = new_terrain_points
# 重建地形碰撞体
self.rebuild_terrain_colliders()
def rebuild_terrain_colliders(self):
"""重新构建地形碰撞体"""
# 移除旧碰撞体
for segment in self.terrain_segments:
self.space.remove(segment.body, segment)
self.terrain_segments.clear()
# 创建新碰撞体
self.create_terrain_colliders()
def create_terrain_debris(self, x, y, power):
"""创建地形碎片"""
num_debris = random.randint(8, 15)
for _ in range(num_debris):
# 碎片位置
angle = random.uniform(0, 2*math.pi)
distance = random.uniform(10, 30)
debris_x = x + math.cos(angle) * distance
debris_y = y + math.sin(angle) * distance
# 获取当前地形高度
terrain_y = self.get_terrain_height(debris_x)
# 创建碎片物理体
mass = random.uniform(0.5, 2.0)
radius = random.uniform(3, 8)
moment = pymunk.moment_for_circle(mass, 0, radius)
body = pymunk.Body(mass, moment)
body.position = (debris_x, terrain_y - 5)
# 初始速度
velocity = power * 0.5
body.velocity = (
math.cos(angle) * velocity + random.uniform(-20, 20),
math.sin(angle) * velocity + random.uniform(-20, 20)
)
# 碎片形状
shape = pymunk.Circle(body, radius)
shape.friction = 0.7
shape.elasticity = 0.3
shape.color = self.get_terrain_color_at(debris_x)
# 添加到空间
self.space.add(body, shape)
8.3 地形渲染优化
地形渲染需要优化,特别是对于动态变化的地形:
python
class UltimateTankBattle:
def draw_terrain(self):
"""绘制地形 - 优化版本"""
if len(self.terrain_points) < 2:
return
# 获取当前地形的颜色
terrain_colors = self.terrain_colors.get(
self.current_terrain,
(COLORS["grass_light"], COLORS["grass_dark"])
)
light_color, dark_color = terrain_colors
# 1. 填充地形多边形
self.draw_terrain_fill(light_color)
# 2. 绘制地形轮廓
self.draw_terrain_outline(dark_color)
# 3. 绘制地形细节
self.draw_terrain_details(light_color)
def draw_terrain_fill(self, fill_color):
"""绘制地形填充"""
# 创建地形多边形顶点列表
terrain_polygon = []
# 添加地形点
terrain_polygon.extend(self.terrain_points)
# 添加屏幕底部两个点形成闭合多边形
terrain_polygon.append((self.screen_width, self.screen_height))
terrain_polygon.append((0, self.screen_height))
# 绘制填充
pygame.draw.polygon(self.screen, fill_color, terrain_polygon)
def draw_terrain_outline(self, outline_color):
"""绘制地形轮廓"""
pygame.draw.lines(self.screen, outline_color, False,
self.terrain_points, 3)
def draw_terrain_details(self, base_color):
"""绘制地形细节(草、石头等)"""
for i in range(len(self.terrain_points) - 1):
x1, y1 = self.terrain_points[i]
x2, y2 = self.terrain_points[i + 1]
# 随机绘制小草
if random.random() < 0.1:
t = random.random()
x = x1 + (x2 - x1) * t
y = y1 + (y2 - y1) * t
grass_height = random.randint(2, 6)
grass_color = (
max(0, base_color[0] - 30),
max(0, base_color[1] - 20),
max(0, base_color[2] - 10)
)
pygame.draw.line(self.screen, grass_color,
(int(x), int(y)),
(int(x), int(y - grass_height)), 1)
9. 弹道预测系统
9.1 物理预测算法
弹道预测是游戏的核心策略工具:
python
class UltimateTankBattle:
def calculate_trajectory(self, start_x, start_y, angle, power,
projectile_type=ProjectileType.STANDARD,
num_points=100):
"""计算弹道轨迹"""
points = []
dt = 0.03
max_time = 8
# 初始速度
v0 = power * 6
vx = v0 * math.cos(angle)
vy = v0 * math.sin(angle)
# 重力加速度
g = 800
# 风力影响
wind_force = self.wind_speed * 0.1
wind_x = math.cos(self.wind_direction) * wind_force
wind_y = math.sin(self.wind_direction) * wind_force
# 空气阻力系数(与炮弹质量相关)
air_resistance = 0.001 * projectile_type.value["mass"]
# 初始位置
x, y = start_x, start_y
time_elapsed = 0
while time_elapsed < max_time and y < self.screen_height:
# 保存当前点
points.append((x, y))
if len(points) >= num_points:
break
# 检查是否碰撞地形
terrain_y = self.get_terrain_height(x)
if y >= terrain_y:
points.append((x, terrain_y))
break
# 物理计算
# 1. 应用重力
vy += g * dt
# 2. 应用风力
vx += wind_x * dt
vy += wind_y * dt
# 3. 应用空气阻力
speed = math.sqrt(vx*vx + vy*vy)
if speed > 0:
drag_force = air_resistance * speed * speed
vx -= (vx / speed) * drag_force * dt
vy -= (vy / speed) * drag_force * dt
# 4. 更新位置
x += vx * dt
y += vy * dt
time_elapsed += dt
return points
def predict_impact_point(self, start_x, start_y, angle, power, projectile_type):
"""预测落点"""
trajectory = self.calculate_trajectory(start_x, start_y, angle,
power, projectile_type)
if trajectory:
return trajectory[-1] # 返回最后一个点(碰撞点)
return None
9.2 弹道可视化
python
class UltimateTankBattle:
def draw_trajectory(self):
"""绘制弹道预测线"""
if not self.show_trajectory or self.game_state not in [GameState.AIMING, GameState.CHARGING]:
return
tank = self.players[self.current_player]["tank"]
if not tank:
return
# 计算发射位置
barrel_length = 30
start_x = tank.body.position.x + math.cos(tank.barrel_angle) * barrel_length
start_y = tank.body.position.y + math.sin(tank.barrel_angle) * barrel_length
# 计算轨迹
trajectory = self.calculate_trajectory(
start_x, start_y,
tank.barrel_angle,
self.current_power,
self.players[self.current_player]["projectile_type"]
)
if len(trajectory) > 1:
self.draw_trajectory_line(trajectory)
self.draw_impact_indicator(trajectory[-1])
def draw_trajectory_line(self, trajectory):
"""绘制轨迹线"""
for i in range(len(trajectory) - 1):
x1, y1 = trajectory[i]
x2, y2 = trajectory[i + 1]
# 转换为整数坐标
try:
x1_int, y1_int = int(x1), int(y1)
x2_int, y2_int = int(x2), int(y2)
except (ValueError, TypeError):
continue
# 计算渐变色
t = i / len(trajectory)
r = int(COLORS["trajectory_start"][0] +
(COLORS["trajectory_end"][0] - COLORS["trajectory_start"][0]) * t)
g = int(COLORS["trajectory_start"][1] +
(COLORS["trajectory_end"][1] - COLORS["trajectory_start"][1]) * t)
b = int(COLORS["trajectory_start"][2] +
(COLORS["trajectory_end"][2] - COLORS["trajectory_start"][2]) * t)
# 计算线宽(逐渐变细)
thickness = max(1, int(3 * (1 - t * 0.7)))
# 绘制线段
pygame.draw.line(self.screen, (r, g, b),
(x1_int, y1_int),
(x2_int, y2_int),
thickness)
# 绘制轨迹点
if i % 3 == 0:
dot_radius = max(1, int(thickness * 0.8))
pygame.draw.circle(self.screen, (r, g, b),
(x1_int, y1_int), dot_radius)
def draw_impact_indicator(self, impact_point):
"""绘制落点指示器"""
if not impact_point:
return
x, y = int(impact_point[0]), int(impact_point[1])
# 绘制十字准星
cross_size = 15
# 水平线
pygame.draw.line(self.screen, (255, 50, 50),
(x - cross_size, y), (x + cross_size, y), 2)
# 垂直线
pygame.draw.line(self.screen, (255, 50, 50),
(x, y - cross_size), (x, y + cross_size), 2)
# 绘制外圈
pygame.draw.circle(self.screen, (255, 50, 50, 100),
(x, y), cross_size + 5, 1)
10. 用户界面系统
10.1 UI组件架构
游戏UI采用分层渲染架构:
python
class UltimateTankBattle:
def draw_ui(self):
"""绘制用户界面"""
if self.game_state == GameState.GAME_OVER:
return
# 1. 绘制状态栏
self.draw_status_bar()
# 2. 绘制信息面板
self.draw_info_panel()
# 3. 绘制控制面板
self.draw_control_panel()
# 4. 绘制游戏状态
self.draw_game_state()
def draw_status_bar(self):
"""绘制顶部状态栏"""
bar_height = 40
bar_surface = pygame.Surface((self.screen_width, bar_height), pygame.SRCALPHA)
bar_surface.fill((0, 0, 0, 120))
# 地形信息
terrain_list = list(TerrainType)
current_index = terrain_list.index(self.current_terrain)
progress_text = f"地形 {current_index + 1}/{len(terrain_list)}: {self.current_terrain.value.capitalize()}"
progress_surface = self.font_small.render(progress_text, True, COLORS["ui_highlight"])
bar_surface.blit(progress_surface, (20, 10))
# 分数信息
score_text = f"玩家1: {self.players[1]['score']} 玩家2: {self.players[2]['score']}"
score_surface = self.font_small.render(score_text, True, COLORS["ui_text"])
bar_surface.blit(score_surface, (self.screen_width - score_surface.get_width() - 20, 10))
self.screen.blit(bar_surface, (0, 0))
10.2 信息面板设计
python
class UltimateTankBattle:
def draw_info_panel(self):
"""绘制左侧信息面板"""
panel_width = 350
panel_height = 500
panel_x = 20
panel_y = 20
# 创建半透明背景
panel_bg = pygame.Surface((panel_width, panel_height), pygame.SRCALPHA)
panel_bg.fill((0, 0, 0, 200))
self.screen.blit(panel_bg, (panel_x, panel_y))
y_offset = panel_y + 20
# 1. 游戏信息
self.draw_game_info(panel_x, panel_y, y_offset)
y_offset += 120
# 2. 玩家状态
y_offset = self.draw_player_status(panel_x, y_offset)
# 3. 当前回合信息
if self.game_state in [GameState.AIMING, GameState.CHARGING, GameState.PLAYING]:
y_offset = self.draw_current_turn_info(panel_x, y_offset)
# 4. 风力指示器
self.draw_wind_indicator(panel_x, y_offset)
def draw_game_info(self, panel_x, panel_y, y_offset):
"""绘制游戏基本信息"""
# 关卡信息
level_text = f"地形: {self.current_terrain.value.capitalize()}"
level_surface = self.font_medium.render(level_text, True, COLORS["ui_highlight"])
self.screen.blit(level_surface, (panel_x + 20, y_offset))
y_offset += 40
# 回合信息
turn_text = f"回合: {self.turn_count + 1}"
turn_surface = self.font_medium.render(turn_text, True, COLORS["ui_text"])
self.screen.blit(turn_surface, (panel_x + 20, y_offset))
y_offset += 40
# 当前玩家
current_text = f"当前玩家: {self.current_player}"
current_surface = self.font_medium.render(current_text, True,
self.players[self.current_player]["color"])
self.screen.blit(current_surface, (panel_x + 20, y_offset))
def draw_player_status(self, panel_x, y_offset):
"""绘制玩家状态"""
for player_id in [1, 2]:
player = self.players[player_id]
tank = player["tank"]
health = tank.health if tank else 0
# 玩家标签
player_text = f"{player['name']}"
if self.current_player == player_id and self.game_state != GameState.GAME_OVER:
player_text += " ←"
player_surface = self.font_medium.render(player_text, True, player["color"])
self.screen.blit(player_surface, (panel_x + 20, y_offset))
# 生命条
y_offset = self.draw_health_bar(panel_x, y_offset, player, health)
return y_offset + 20
def draw_health_bar(self, panel_x, y_offset, player, health):
"""绘制生命条"""
health_bar_width = 180
health_bar_height = 20
health_bar_x = panel_x + 140
health_bar_y = y_offset
# 背景
pygame.draw.rect(self.screen, (50, 50, 50),
(health_bar_x, health_bar_y, health_bar_width, health_bar_height))
# 计算填充宽度
health_percent = health / player["max_health"]
health_fill_width = int(health_bar_width * health_percent)
# 选择颜色
if health_percent > 0.6:
health_color = COLORS["health_good"]
elif health_percent > 0.3:
health_color = COLORS["health_warning"]
else:
health_color = COLORS["health_critical"]
# 填充
pygame.draw.rect(self.screen, health_color,
(health_bar_x, health_bar_y, health_fill_width, health_bar_height))
# 文字
health_text = f"{int(health)}/{player['max_health']}"
health_surface = self.font_small.render(health_text, True, COLORS["ui_text"])
self.screen.blit(health_surface, (health_bar_x + health_bar_width + 10, health_bar_y))
return y_offset + 40
def draw_current_turn_info(self, panel_x, y_offset):
"""绘制当前回合信息"""
tank = self.players[self.current_player]["tank"]
if not tank:
return y_offset
# 角度显示
angle_deg = math.degrees(tank.barrel_angle)
angle_text = f"角度: {angle_deg:+.1f}°"
angle_surface = self.font_medium.render(angle_text, True, COLORS["ui_text"])
self.screen.blit(angle_surface, (panel_x + 20, y_offset))
y_offset += 35
# 力量显示
power_text = f"力量: {self.current_power:.1f}"
power_surface = self.font_medium.render(power_text, True, COLORS["ui_text"])
self.screen.blit(power_surface, (panel_x + 20, y_offset))
y_offset += 35
# 弹药类型
projectile_type = self.players[self.current_player]["projectile_type"].value
type_text = f"弹药: {projectile_type['name']}"
type_surface = self.font_small.render(type_text, True, projectile_type["color"])
self.screen.blit(type_surface, (panel_x + 20, y_offset))
return y_offset + 30
def draw_wind_indicator(self, panel_x, y_offset):
"""绘制风力指示器"""
# 风力文本
wind_text = f"风速: {self.wind_speed:+.1f} m/s"
wind_surface = self.font_small.render(wind_text, True, COLORS["ui_text"])
self.screen.blit(wind_surface, (panel_x + 20, y_offset))
# 风向指示器
wind_x = panel_x + 150
wind_y = y_offset + 5
wind_length = 40
# 计算风向向量
wind_vector_x = math.cos(self.wind_direction) * wind_length * (abs(self.wind_speed) / 30)
wind_vector_y = math.sin(self.wind_direction) * wind_length * (abs(self.wind_speed) / 30)
# 选择颜色
color = COLORS["wind_positive"] if self.wind_speed > 0 else COLORS["wind_negative"]
# 绘制风向箭头
pygame.draw.line(self.screen, color,
(wind_x, wind_y),
(int(wind_x + wind_vector_x), int(wind_y + wind_vector_y)), 3)
# 绘制箭头头部
if abs(self.wind_speed) > 0.1:
self.draw_arrow_head(wind_x, wind_y, wind_vector_x, wind_vector_y, color)
10.3 控制面板设计
python
class UltimateTankBattle:
def draw_control_panel(self):
"""绘制右侧控制面板"""
panel_width = 320
panel_height = 450
panel_x = self.screen_width - panel_width - 20
panel_y = 20
# 面板背景
panel_bg = pygame.Surface((panel_width, panel_height), pygame.SRCALPHA)
panel_bg.fill((0, 0, 0, 200))
self.screen.blit(panel_bg, (panel_x, panel_y))
y_offset = panel_y + 20
# 标题
title = "游戏控制"
title_surface = self.font_medium.render(title, True, COLORS["ui_highlight"])
self.screen.blit(title_surface, (panel_x + 20, y_offset))
y_offset += 40
# 控制说明
controls = [
("鼠标移动", "瞄准"),
("鼠标左键", "蓄力/发射"),
("空格键", "直接发射"),
("W/S 或 ↑/↓", "调整角度"),
("A/D 或 ←/→", "移动坦克"),
("Q/E", "增加/减少力量"),
("T", "切换轨迹预测"),
("R", "重新开始游戏"),
("N", "切换地形"),
("1/2", "切换炮弹类型"),
("ESC", "退出游戏")
]
for key_text, action_text in controls:
# 按键文本
key_surface = self.font_tiny.render(key_text, True, (200, 230, 255))
self.screen.blit(key_surface, (panel_x + 20, y_offset))
# 分隔符
sep_surface = self.font_tiny.render(":", True, (200, 200, 200))
self.screen.blit(sep_surface, (panel_x + 100, y_offset))
# 功能文本
action_surface = self.font_tiny.render(action_text, True, (220, 220, 220))
self.screen.blit(action_surface, (panel_x + 120, y_offset))
y_offset += 28
y_offset += 20
# 当前状态
state_text = f"状态: {self.game_state.value}"
state_surface = self.font_small.render(state_text, True, COLORS["ui_highlight"])
self.screen.blit(state_surface, (panel_x + 20, y_offset))
11. 游戏状态管理系统
11.1 状态机设计
游戏采用有限状态机(FSM)管理游戏流程:
python
class GameState(Enum):
"""游戏状态枚举"""
PLAYING = "游戏中" # 主游戏状态
AIMING = "瞄准中" # 瞄准状态
CHARGING = "蓄力中" # 蓄力状态
FIRING = "发射中" # 发射动画状态
WAITING = "等待炮弹落地" # 等待炮弹爆炸
TURN_SWITCH = "切换回合" # 回合切换状态
GAME_OVER = "游戏结束" # 游戏结束状态
MENU = "主菜单" # 菜单状态
PAUSED = "暂停" # 游戏暂停
11.2 状态转换逻辑
状态机管理游戏流程的转换:
python
class UltimateTankBattle:
def update_game_state(self, new_state):
"""更新游戏状态"""
old_state = self.game_state
self.game_state = new_state
# 状态转换处理
if new_state == GameState.AIMING:
self.on_enter_aiming(old_state)
elif new_state == GameState.CHARGING:
self.on_enter_charging(old_state)
elif new_state == GameState.FIRING:
self.on_enter_firing(old_state)
elif new_state == GameState.WAITING:
self.on_enter_waiting(old_state)
elif new_state == GameState.TURN_SWITCH:
self.on_enter_turn_switch(old_state)
elif new_state == GameState.GAME_OVER:
self.on_enter_game_over(old_state)
elif new_state == GameState.MENU:
self.on_enter_menu(old_state)
elif new_state == GameState.PAUSED:
self.on_enter_paused(old_state)
11.3 状态处理函数
每个状态都有对应的进入、更新、退出处理:
python
class UltimateTankBattle:
def handle_state_transitions(self, dt):
"""处理状态转换逻辑"""
current_state = self.game_state
if current_state == GameState.AIMING:
self.handle_aiming_state(dt)
elif current_state == GameState.CHARGING:
self.handle_charging_state(dt)
elif current_state == GameState.FIRING:
self.handle_firing_state(dt)
elif current_state == GameState.WAITING:
self.handle_waiting_state(dt)
elif current_state == GameState.TURN_SWITCH:
self.handle_turn_switch_state(dt)
elif current_state == GameState.GAME_OVER:
self.handle_game_over_state(dt)
elif current_state == GameState.MENU:
self.handle_menu_state(dt)
elif current_state == GameState.PAUSED:
self.handle_paused_state(dt)
11.4 瞄准状态处理
python
class UltimateTankBattle:
def handle_aiming_state(self, dt):
"""处理瞄准状态"""
tank = self.players[self.current_player]["tank"]
if not tank:
return
# 获取鼠标位置
mouse_x, mouse_y = pygame.mouse.get_pos()
# 瞄准目标
tank.aim_at(mouse_x, mouse_y)
# 计算弹道预测
if self.show_trajectory:
self.trajectory_points = self.calculate_trajectory(
tank.body.position.x + math.cos(tank.barrel_angle) * 30,
tank.body.position.y + math.sin(tank.barrel_angle) * 30,
tank.barrel_angle,
self.current_power,
self.players[self.current_player]["projectile_type"]
)
# 检查状态转换
for event in self.current_events:
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
# 鼠标左键按下,开始蓄力
self.update_game_state(GameState.CHARGING)
self.charge_start_time = pygame.time.get_ticks()
break
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
# 空格键立即发射
self.fire_projectile()
self.update_game_state(GameState.FIRING)
elif event.key == pygame.K_a or event.key == pygame.K_LEFT:
# 向左移动
tank.move(-300)
elif event.key == pygame.K_d or event.key == pygame.K_RIGHT:
# 向右移动
tank.move(300)
elif event.key == pygame.K_e:
# 增加力量
self.current_power = min(100, self.current_power + 5)
elif event.key == pygame.K_q:
# 减少力量
self.current_power = max(0, self.current_power - 5)
elif event.key == pygame.K_1:
# 切换为标准弹
self.players[self.current_player]["projectile_type"] = ProjectileType.STANDARD
elif event.key == pygame.K_2:
# 切换为重型弹
self.players[self.current_player]["projectile_type"] = ProjectileType.HEAVY
elif event.key == pygame.K_3:
# 切换为高爆弹
self.players[self.current_player]["projectile_type"] = ProjectileType.EXPLOSIVE
elif event.key == pygame.K_t:
# 切换弹道显示
self.show_trajectory = not self.show_trajectory
11.5 蓄力状态处理
python
class UltimateTankBattle:
def handle_charging_state(self, dt):
"""处理蓄力状态"""
current_time = pygame.time.get_ticks()
charge_time = (current_time - self.charge_start_time) / 1000.0
# 蓄力进度(0-1)
charge_progress = min(1.0, charge_time / 2.0) # 2秒充满
self.current_power = charge_progress * 100
# 力量指示器效果
self.power_indicator_alpha = 128 + int(127 * abs(math.sin(current_time * 0.01)))
# 检查发射条件
for event in self.current_events:
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
# 鼠标释放,发射炮弹
self.fire_projectile()
self.update_game_state(GameState.FIRING)
break
elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
# ESC取消发射
self.update_game_state(GameState.AIMING)
break
def draw_power_indicator(self):
"""绘制力量指示器"""
if self.game_state != GameState.CHARGING:
return
tank = self.players[self.current_player]["tank"]
if not tank:
return
# 计算炮管末端位置
barrel_length = 30
start_x = tank.body.position.x + math.cos(tank.barrel_angle) * barrel_length
start_y = tank.body.position.y + math.sin(tank.barrel_angle) * barrel_length
# 计算力量指示器位置
indicator_length = 20 + self.current_power * 2
end_x = start_x + math.cos(tank.barrel_angle) * indicator_length
end_y = start_y + math.sin(tank.barrel_angle) * indicator_length
# 计算颜色(根据力量变化)
r = int(255 * (self.current_power / 100))
g = int(255 * (1 - self.current_power / 100))
b = 0
# 透明度效果
alpha = min(255, self.power_indicator_alpha)
# 创建半透明表面
indicator_surface = pygame.Surface((self.screen_width, self.screen_height), pygame.SRCALPHA)
# 绘制指示线
pygame.draw.line(indicator_surface, (r, g, b, alpha),
(int(start_x), int(start_y)),
(int(end_x), int(end_y)), 4)
# 绘制力量条
bar_width = 200
bar_height = 20
bar_x = self.screen_width // 2 - bar_width // 2
bar_y = 50
# 背景
pygame.draw.rect(indicator_surface, (50, 50, 50, 200),
(bar_x, bar_y, bar_width, bar_height))
# 填充
fill_width = int(bar_width * (self.current_power / 100))
pygame.draw.rect(indicator_surface, (r, g, b, 200),
(bar_x, bar_y, fill_width, bar_height))
# 边框
pygame.draw.rect(indicator_surface, (255, 255, 255, 200),
(bar_x, bar_y, bar_width, bar_height), 2)
# 文字
power_text = f"力量: {self.current_power:.1f}"
font = pygame.font.Font(None, 24)
text_surface = font.render(power_text, True, (255, 255, 255))
text_rect = text_surface.get_rect(center=(self.screen_width//2, bar_y - 15))
indicator_surface.blit(text_surface, text_rect)
# 绘制到屏幕
self.screen.blit(indicator_surface, (0, 0))
11.6 发射状态处理
python
class UltimateTankBattle:
def fire_projectile(self):
"""发射炮弹"""
tank = self.players[self.current_player]["tank"]
if not tank:
return
# 计算发射位置
barrel_length = 30
start_x = tank.body.position.x + math.cos(tank.barrel_angle) * barrel_length
start_y = tank.body.position.y + math.sin(tank.barrel_angle) * barrel_length
# 创建炮弹
projectile_type = self.players[self.current_player]["projectile_type"]
projectile = Projectile(
x=start_x,
y=start_y,
angle=tank.barrel_angle,
power=self.current_power,
projectile_type=projectile_type,
space=self.space
)
# 添加炮弹尾迹
projectile.trail_particles = []
# 添加到列表
self.projectiles.append(projectile)
# 播放音效
self.play_sound("fire")
# 重置力量
self.current_power = 60.0
def handle_firing_state(self, dt):
"""处理发射状态"""
# 等待所有炮弹发射
if len(self.projectiles) > 0:
# 仍有炮弹在飞行
self.update_game_state(GameState.WAITING)
else:
# 没有炮弹,返回瞄准状态
self.update_game_state(GameState.AIMING)
11.7 等待状态处理
python
class UltimateTankBattle:
def handle_waiting_state(self, dt):
"""处理等待状态 - 等待炮弹落地或爆炸"""
# 更新所有炮弹
for projectile in self.projectiles[:]:
if not projectile.active:
# 炮弹已爆炸,但可能还有粒子效果
if len(projectile.trail_particles) == 0:
self.projectiles.remove(projectile)
continue
# 检查是否可以切换到回合切换状态
can_switch = True
# 条件1:没有活跃的炮弹
for projectile in self.projectiles:
if projectile.active:
can_switch = False
break
# 条件2:没有活跃的爆炸效果
if len(self.explosions) > 0:
can_switch = False
# 条件3:没有活跃的粒子效果
if len(self.particles) > 0:
can_switch = False
# 条件4:物理世界稳定
if self.check_physics_stable() and can_switch:
# 延迟一段时间确保效果结束
if not hasattr(self, 'switch_timer'):
self.switch_timer = 1.0
else:
self.switch_timer -= dt
if self.switch_timer <= 0:
self.update_game_state(GameState.TURN_SWITCH)
del self.switch_timer
else:
if hasattr(self, 'switch_timer'):
del self.switch_timer
def check_physics_stable(self):
"""检查物理世界是否稳定"""
# 检查所有坦克是否稳定
for player_id, player_data in self.players.items():
tank = player_data["tank"]
if tank:
velocity = tank.body.velocity
angular_velocity = tank.body.angular_velocity
# 速度和角速度是否足够小
if velocity.length > 5.0 or abs(angular_velocity) > 0.1:
return False
return True
11.8 回合切换状态处理
python
class UltimateTankBattle:
def handle_turn_switch_state(self, dt):
"""处理回合切换状态"""
# 清理上一回合的残余效果
self.cleanup_round()
# 检查游戏是否结束
if self.check_game_over():
self.update_game_state(GameState.GAME_OVER)
return
# 切换到下一个玩家
self.switch_to_next_player()
# 重置当前回合参数
self.current_power = 60.0
# 短暂延迟后进入瞄准状态
if not hasattr(self, 'switch_delay_timer'):
self.switch_delay_timer = 1.0
else:
self.switch_delay_timer -= dt
if self.switch_delay_timer <= 0:
self.update_game_state(GameState.AIMING)
del self.switch_delay_timer
def cleanup_round(self):
"""清理回合残余"""
# 清理不活跃的炮弹
self.projectiles = [p for p in self.projectiles if p.active]
# 清理爆炸效果
self.explosions = [e for e in self.explosions if e.alpha > 0]
# 清理粒子
self.particles = [p for p in self.particles if p["life"] > 0]
# 清理超出屏幕的对象
for projectile in self.projectiles[:]:
if projectile.body.position.y > self.screen_height + 100:
self.projectiles.remove(projectile)
def check_game_over(self):
"""检查游戏是否结束"""
alive_count = 0
for player_id, player_data in self.players.items():
tank = player_data["tank"]
if tank and tank.health > 0:
alive_count += 1
return alive_count <= 1
def switch_to_next_player(self):
"""切换到下一个玩家"""
# 保存当前玩家
self.previous_player = self.current_player
# 找到下一个存活的玩家
original_player = self.current_player
while True:
self.current_player = 3 - self.current_player # 在1和2之间切换
tank = self.players[self.current_player]["tank"]
if tank and tank.health > 0:
break
if self.current_player == original_player:
# 回到原始玩家,说明只有一个玩家存活
break
# 增加回合计数
self.turn_count += 1
# 随机改变风力
self.change_wind()
11.9 游戏结束状态处理
python
class UltimateTankBattle:
def handle_game_over_state(self, dt):
"""处理游戏结束状态"""
# 确定胜利者
winner = None
for player_id, player_data in self.players.items():
tank = player_data["tank"]
if tank and tank.health > 0:
winner = player_id
break
# 绘制游戏结束界面
self.draw_game_over_screen(winner)
# 处理重置游戏
for event in self.current_events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r:
# 重新开始游戏
self.reset_game()
self.update_game_state(GameState.AIMING)
elif event.key == pygame.K_ESCAPE:
# 返回菜单
self.update_game_state(GameState.MENU)
def draw_game_over_screen(self, winner):
"""绘制游戏结束屏幕"""
# 创建半透明覆盖层
overlay = pygame.Surface((self.screen_width, self.screen_height), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 180))
self.screen.blit(overlay, (0, 0))
# 游戏结束文本
if winner:
text = f"游戏结束!玩家{winner}胜利!"
color = self.players[winner]["color"]
else:
text = "游戏结束!平局!"
color = (255, 255, 255)
# 大标题
title_surface = self.font_large.render("游戏结束", True, (255, 50, 50))
title_rect = title_surface.get_rect(center=(self.screen_width//2, 200))
self.screen.blit(title_surface, title_rect)
# 胜利者文本
winner_surface = self.font_medium.render(text, True, color)
winner_rect = winner_surface.get_rect(center=(self.screen_width//2, 280))
self.screen.blit(winner_surface, winner_rect)
# 分数显示
score_text = f"最终分数 - 玩家1: {self.players[1]['score']} 玩家2: {self.players[2]['score']}"
score_surface = self.font_medium.render(score_text, True, (255, 255, 255))
score_rect = score_surface.get_rect(center=(self.screen_width//2, 340))
self.screen.blit(score_surface, score_rect)
# 回合数
turn_text = f"总回合数: {self.turn_count}"
turn_surface = self.font_small.render(turn_text, True, (200, 200, 200))
turn_rect = turn_surface.get_rect(center=(self.screen_width//2, 380))
self.screen.blit(turn_surface, turn_rect)
# 操作提示
restart_surface = self.font_small.render("按 R 重新开始游戏", True, (150, 255, 150))
restart_rect = restart_surface.get_rect(center=(self.screen_width//2, 450))
self.screen.blit(restart_surface, restart_rect)
menu_surface = self.font_small.render("按 ESC 返回菜单", True, (150, 150, 255))
menu_rect = menu_surface.get_rect(center=(self.screen_width//2, 490))
self.screen.blit(menu_surface, menu_rect)
11.10 主菜单状态处理
python
class UltimateTankBattle:
def handle_menu_state(self, dt):
"""处理主菜单状态"""
# 绘制菜单背景
self.screen.fill(COLORS["sky"])
# 绘制地形预览
self.draw_menu_terrain()
# 绘制菜单选项
self.draw_menu_options()
# 处理菜单输入
self.handle_menu_input()
def draw_menu_terrain(self):
"""绘制菜单地形预览"""
# 简化版地形绘制
points = []
for i in range(20):
x = (i / 20) * self.screen_width
y = 400 + math.sin(i * 0.5) * 50
points.append((x, y))
# 绘制地形
pygame.draw.polygon(self.screen, COLORS["grass_light"],
points + [(self.screen_width, self.screen_height), (0, self.screen_height)])
pygame.draw.lines(self.screen, COLORS["grass_dark"], False, points, 3)
def draw_menu_options(self):
"""绘制菜单选项"""
# 游戏标题
title_surface = self.font_title.render("终极坦克大作战", True, (255, 100, 50))
title_rect = title_surface.get_rect(center=(self.screen_width//2, 150))
self.screen.blit(title_surface, title_rect)
# 选项列表
options = [
{"text": "开始游戏", "action": "start"},
{"text": "游戏说明", "action": "help"},
{"text": "设置", "action": "settings"},
{"text": "退出游戏", "action": "quit"}
]
# 绘制选项
for i, option in enumerate(options):
y_pos = 300 + i * 60
color = (255, 255, 255) if i != self.selected_menu_item else (255, 200, 50)
# 绘制选项背景
if i == self.selected_menu_item:
pygame.draw.rect(self.screen, (0, 0, 0, 128),
(self.screen_width//2 - 150, y_pos - 25, 300, 50))
# 绘制选项文本
option_surface = self.font_medium.render(option["text"], True, color)
option_rect = option_surface.get_rect(center=(self.screen_width//2, y_pos))
self.screen.blit(option_surface, option_rect)
def handle_menu_input(self):
"""处理菜单输入"""
for event in self.current_events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
self.selected_menu_item = (self.selected_menu_item - 1) % 4
elif event.key == pygame.K_DOWN:
self.selected_menu_item = (self.selected_menu_item + 1) % 4
elif event.key == pygame.K_RETURN or event.key == pygame.K_SPACE:
self.select_menu_item(self.selected_menu_item)
elif event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
def select_menu_item(self, index):
"""选择菜单项"""
if index == 0: # 开始游戏
self.reset_game()
self.update_game_state(GameState.AIMING)
elif index == 1: # 游戏说明
self.update_game_state(GameState.HELP)
elif index == 2: # 设置
self.update_game_state(GameState.SETTINGS)
elif index == 3: # 退出游戏
pygame.quit()
sys.exit()
11.11 暂停状态处理
python
class UltimateTankBattle:
def handle_paused_state(self, dt):
"""处理暂停状态"""
# 绘制游戏暂停覆盖层
overlay = pygame.Surface((self.screen_width, self.screen_height), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
self.screen.blit(overlay, (0, 0))
# 暂停文本
pause_surface = self.font_large.render("游戏暂停", True, (255, 255, 100))
pause_rect = pause_surface.get_rect(center=(self.screen_width//2, 200))
self.screen.blit(pause_surface, pause_rect)
# 操作提示
resume_surface = self.font_medium.render("按 P 继续游戏", True, (150, 255, 150))
resume_rect = resume_surface.get_rect(center=(self.screen_width//2, 280))
self.screen.blit(resume_surface, resume_rect)
menu_surface = self.font_medium.render("按 ESC 返回菜单", True, (150, 150, 255))
menu_rect = menu_surface.get_rect(center=(self.screen_width//2, 330))
self.screen.blit(menu_surface, menu_rect)
# 处理输入
for event in self.current_events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_p:
# 继续游戏
self.update_game_state(self.previous_state)
elif event.key == pygame.K_ESCAPE:
# 返回菜单
self.update_game_state(GameState.MENU)
12. 游戏主循环设计
12.1 主循环架构
python
class UltimateTankBattle:
def run(self):
"""游戏主循环"""
clock = pygame.time.Clock()
while self.running:
# 计算增量时间
dt = clock.tick(60) / 1000.0
# 处理事件
self.current_events = pygame.event.get()
self.handle_events()
# 更新游戏状态
self.update(dt)
# 绘制游戏
self.draw()
# 更新显示
pygame.display.flip()
# 控制帧率
clock.tick(60)
pygame.quit()
def handle_events(self):
"""处理所有事件"""
for event in self.current_events:
# 通用事件处理
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
if self.game_state in [GameState.AIMING, GameState.CHARGING, GameState.WAITING]:
# 游戏过程中按ESC暂停
self.previous_state = self.game_state
self.update_game_state(GameState.PAUSED)
elif self.game_state == GameState.PAUSED:
# 暂停时按ESC返回菜单
self.update_game_state(GameState.MENU)
elif event.key == pygame.K_p:
if self.game_state in [GameState.AIMING, GameState.CHARGING, GameState.WAITING]:
# 游戏过程中按P暂停
self.previous_state = self.game_state
self.update_game_state(GameState.PAUSED)
# 状态特定事件处理
if self.game_state == GameState.AIMING:
self.handle_aiming_events(event)
elif self.game_state == GameState.CHARGING:
self.handle_charging_events(event)
# ... 其他状态的事件处理
def update(self, dt):
"""更新游戏逻辑"""
# 更新物理引擎
self.space.step(dt)
# 更新游戏状态
self.handle_state_transitions(dt)
# 更新游戏对象
self.update_game_objects(dt)
# 更新UI
self.update_ui(dt)
def update_game_objects(self, dt):
"""更新游戏对象"""
# 更新炮弹
for projectile in self.projectiles[:]:
if projectile.active:
projectile.update(dt, self.tanks, self.terrain_points, self.obstacles)
# 更新爆炸效果
for explosion in self.explosions[:]:
if not explosion.update(dt):
self.explosions.remove(explosion)
# 更新粒子
for particle in self.particles[:]:
if not self.update_particle(particle, dt):
self.particles.remove(particle)
# 更新坦克
for tank in self.tanks:
if tank:
tank.update(dt)
def draw(self):
"""绘制游戏"""
# 清除屏幕
self.screen.fill(COLORS["sky"])
# 绘制游戏对象
self.draw_terrain()
self.draw_obstacles()
self.draw_tanks()
self.draw_projectiles()
self.draw_particles()
self.draw_explosions()
# 绘制UI
if self.game_state != GameState.MENU:
self.draw_ui()
self.draw_trajectory()
self.draw_power_indicator()
# 绘制状态特定UI
if self.game_state == GameState.GAME_OVER:
self.draw_game_over_screen()
elif self.game_state == GameState.MENU:
self.draw_menu()
elif self.game_state == GameState.PAUSED:
self.draw_paused_screen()
3. 总结与展望
13.1 技术总结
本项目成功实现了:
-
完整的物理模拟系统:基于Pymunk的2D物理引擎
-
模块化游戏架构:清晰的状态机和对象管理系统
-
丰富的视觉效果:粒子系统、爆炸效果、轨迹预测
-
用户友好的UI:直观的界面和操作反馈
-
可扩展的游戏机制:多种地形、炮弹类型
13.2 优化空间
-
性能优化:
-
实现对象池减少内存分配
-
优化碰撞检测算法
-
批处理渲染
-
-
功能扩展:
-
添加更多炮弹类型
-
实现网络多人对战
-
添加关卡编辑器
-
支持Mod系统
-
-
视觉效果提升:
-
添加更复杂的粒子效果
-
实现物理材质系统
-
添加天气效果
-
13.3 学习收获
通过本项目,开发者可以学习到:
-
物理引擎的核心概念和应用
-
2D游戏开发的全流程
-
状态机在游戏开发中的应用
-
粒子系统和视觉效果的实现
-
用户界面和交互设计
-
性能优化和调试技巧
这个项目展示了如何将物理模拟与游戏开发相结合,创建出既有教育意义又有娱乐性的游戏体验。