用《坦克大战》学透 Python 面向对象编程
从一份 500 行的 pygame 小游戏源码,看懂类、封装、继承与多态
很多初学者在学完 Python 基础语法后,面对"面向对象"这四个字总觉得抽象。类是什么?对象又是什么?为什么同样的功能,非要用类来写?这篇文章用一个完整的《坦克大战》小游戏源码作为例子,把面向对象编程拆成看得见、改得动的代码块。
为什么选游戏作为入门案例
游戏天然包含多个独立角色:玩家、敌人、子弹、道具、地图格子。每个角色有自己的状态和行为,这正好对应面向对象里的"对象"。如果我们把所有逻辑都堆在一个大函数里,随着功能增加,代码会迅速失控。而把这些角色抽象成类,每个类只负责自己的逻辑,代码就会清晰很多。
本文分析的源码是一个单文件 pygame 实现的《坦克大战》,支持单人/双人合作、多关卡、敌人 AI、道具掉落和粒子特效。你可以在读完之后直接运行它,也可以把它当成改造练习:加一种新坦克、换一种新子弹、设计一个新道具。
项目运行与整体结构
运行这款游戏只需要两步:安装 pygame,然后执行 Python 文件。
# 安装依赖
pip install pygame
# 运行游戏
python 坦克大战.py
源码虽然只有 500 多行,但已经包含了一个完整游戏所需的核心模块。下面这张表列出了主要类及其职责。
| Tank | 管理位置、方向、生命值、移动、射击、碰撞检测 | 玩家坦克和敌方坦克 |
| Bullet | 处理子弹飞行、反弹、命中判定、墙体破坏 | 坦克发射的炮弹 |
| Particle | 管理粒子位置、速度、生命周期和绘制 | 爆炸、击中特效 |
| PowerUp | 处理道具出现、被拾取后的效果 | 回血、冰冻、急速射击 |
| Game | 控制关卡流程、生成敌人、更新全部对象、绘制画面 | 整个游戏世界 |
面向对象的三根支柱
在深入源码之前,先快速回顾三个核心概念。它们不是 Python 独有的,而是所有面向对象语言共通的设计思想。
- 封装:把数据(属性)和操作数据的方法打包到一个类里,外部不需要知道内部细节。
- 继承:子类可以复用父类的属性和方法,同时扩展自己的特性。
- 多态:不同类型的对象可以以相同的方式被调用,表现出各自的行为。
这款坦克大战的代码把这三种思想都用到了。接下来我们逐个类看它们是如何落地的。
类图总览
下图展示了游戏中几个核心类的关系。箭头表示依赖:例如 Game 管理多个 Tank 和 Bullet 实例,而 Bullet 在被创建时需要知道它的发射者。
#mermaid-svg-o4ua2a9WpUvxJx4S{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-o4ua2a9WpUvxJx4S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-o4ua2a9WpUvxJx4S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-o4ua2a9WpUvxJx4S .error-icon{fill:#552222;}#mermaid-svg-o4ua2a9WpUvxJx4S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-o4ua2a9WpUvxJx4S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-o4ua2a9WpUvxJx4S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-o4ua2a9WpUvxJx4S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-o4ua2a9WpUvxJx4S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-o4ua2a9WpUvxJx4S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-o4ua2a9WpUvxJx4S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-o4ua2a9WpUvxJx4S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-o4ua2a9WpUvxJx4S .marker.cross{stroke:#333333;}#mermaid-svg-o4ua2a9WpUvxJx4S svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-o4ua2a9WpUvxJx4S p{margin:0;}#mermaid-svg-o4ua2a9WpUvxJx4S g.classGroup text{fill:#9370DB;stroke:none;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-o4ua2a9WpUvxJx4S g.classGroup text .title{font-weight:bolder;}#mermaid-svg-o4ua2a9WpUvxJx4S .cluster-label text{fill:#333;}#mermaid-svg-o4ua2a9WpUvxJx4S .cluster-label span{color:#333;}#mermaid-svg-o4ua2a9WpUvxJx4S .cluster-label span p{background-color:transparent;}#mermaid-svg-o4ua2a9WpUvxJx4S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-o4ua2a9WpUvxJx4S .cluster text{fill:#333;}#mermaid-svg-o4ua2a9WpUvxJx4S .cluster span{color:#333;}#mermaid-svg-o4ua2a9WpUvxJx4S .nodeLabel,#mermaid-svg-o4ua2a9WpUvxJx4S .edgeLabel{color:#131300;}#mermaid-svg-o4ua2a9WpUvxJx4S .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-o4ua2a9WpUvxJx4S .label text{fill:#131300;}#mermaid-svg-o4ua2a9WpUvxJx4S .labelBkg{background:#ECECFF;}#mermaid-svg-o4ua2a9WpUvxJx4S .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-o4ua2a9WpUvxJx4S .classTitle{font-weight:bolder;}#mermaid-svg-o4ua2a9WpUvxJx4S .node rect,#mermaid-svg-o4ua2a9WpUvxJx4S .node circle,#mermaid-svg-o4ua2a9WpUvxJx4S .node ellipse,#mermaid-svg-o4ua2a9WpUvxJx4S .node polygon,#mermaid-svg-o4ua2a9WpUvxJx4S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-o4ua2a9WpUvxJx4S .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S g.clickable{cursor:pointer;}#mermaid-svg-o4ua2a9WpUvxJx4S g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-o4ua2a9WpUvxJx4S g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-o4ua2a9WpUvxJx4S .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-o4ua2a9WpUvxJx4S .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-o4ua2a9WpUvxJx4S .dashed-line{stroke-dasharray:3;}#mermaid-svg-o4ua2a9WpUvxJx4S .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-o4ua2a9WpUvxJx4S #compositionStart,#mermaid-svg-o4ua2a9WpUvxJx4S .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S #compositionEnd,#mermaid-svg-o4ua2a9WpUvxJx4S .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S #dependencyStart,#mermaid-svg-o4ua2a9WpUvxJx4S .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S #dependencyStart,#mermaid-svg-o4ua2a9WpUvxJx4S .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S #extensionStart,#mermaid-svg-o4ua2a9WpUvxJx4S .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S #extensionEnd,#mermaid-svg-o4ua2a9WpUvxJx4S .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S #aggregationStart,#mermaid-svg-o4ua2a9WpUvxJx4S .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S #aggregationEnd,#mermaid-svg-o4ua2a9WpUvxJx4S .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S #lollipopStart,#mermaid-svg-o4ua2a9WpUvxJx4S .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S #lollipopEnd,#mermaid-svg-o4ua2a9WpUvxJx4S .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-o4ua2a9WpUvxJx4S .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-o4ua2a9WpUvxJx4S .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-o4ua2a9WpUvxJx4S .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-o4ua2a9WpUvxJx4S .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-o4ua2a9WpUvxJx4S :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
shoot
1
1
1
1
1
*
*
*
*
*
Tank
+x, y
+direction
+health
+team
+move(direction, game)
+shoot(game)
+draw(surface, offset)
Bullet
+x, y
+direction
+owner
+speed
+update(game)
+draw(surface, offset)
Particle
+x, y
+vx, vy
+life
+update()
+draw(surface)
PowerUp
+x, y
+kind
+life
+update(game)
+draw(surface, offset)
Game
+grid
+tanks
+bullets
+particles
+powerups
+update(keys)
+draw()
图 1:《坦克大战》核心类关系图
Tank 类:把"坦克"抽象成一个对象
Tank 是游戏里最核心的类。它同时被玩家和敌人复用,通过 team 和 player_number 区分身份。这种设计避免了写两套几乎一样的代码。
class Tank:
def __init__(self, x, y, team, tier=1, player_number=0):
self.x, self.y = x, y
self.team = team
self.tier = tier
self.player_number = player_number
self.direction = 0 if team == "player" else 2
self.size = 30
self.speed = 3.0 if team == "player" else 1.65 + tier * .12
self.health = 2 if team == "player" else tier
self.cooldown = 0
self.dead = False
构造函数 __init__ 把坦克的所有状态都集中初始化:坐标、阵营、等级、方向、速度、生命值。这就是封装:坦克知道自己长什么样、能跑多快、还剩多少血,外部代码不需要手动维护这些变量。
Tank 还提供了统一的行为接口:
- move(direction, game):按方向移动,并处理碰撞。
- shoot(game):创建一颗子弹,并进入冷却。
- update(game, keys):每帧更新状态,包括玩家输入或敌人 AI。
- draw(surface, offset):把自己画到屏幕上。
注意 update 方法里的分支:玩家坦克读取键盘输入,敌人坦克执行 AI 逻辑。虽然内部行为不同,但对外接口完全一样。这正是多态的体现:调用方只需要说"更新",不同类型的坦克会各自做出正确的反应。
Bullet 类:独立的生命周期管理
子弹和坦克是两个完全不同的概念。子弹有飞行方向、速度、反弹次数和所有者;它会在碰到墙时反弹,碰到坦克时造成伤害,反弹超过三次后消失。
class Bullet:
def __init__(self, x, y, direction, owner):
self.x, self.y = x, y
self.direction = direction
self.owner = owner
self.speed = 7.4 if owner.team == "player" else 5.4
self.bounces = 0
self.dead = False
def update(self, game):
# 移动、碰撞、反弹、命中的完整逻辑
...
把子弹单独抽象成类的好处是,Game 不需要关心每一颗子弹怎么飞。它只需要维护一个子弹列表,每帧调用每颗子弹的 update 方法,再清理掉已经死亡的子弹。
Particle 与 PowerUp:小而专一的类
Particle 负责爆炸和击中特效。每个粒子有自己的位置、速度、颜色和剩余生命周期。它只做两件事:更新自己的状态,把自己画出来。
class Particle:
def update(self):
self.x += self.vx
self.y += self.vy
self.life -= 1
def draw(self, surface):
radius = max(1, self.life // 10)
pygame.draw.circle(surface, self.color, (int(self.x), int(self.y)), radius)
PowerUp 处理道具系统。它的 kind 字段决定类型:回血、冻结敌人或加速射击。当玩家碰到道具时,update 方法根据类型触发不同效果,然后让道具消失。
这两个类都很小,但职责非常清晰。它们展示了面向对象设计的一个重要原则:一个类应该只有一个引起它变化的原因。粒子只关心视觉特效,道具只关心拾取效果。
Game 类:整个游戏世界的调度中心
Game 类不直接代表某个可见物体,而是负责把所有对象组织起来。它维护地图、玩家、敌人、子弹、粒子和道具的集合,并在每一帧里协调它们的更新和绘制。
class Game:
def __init__(self):
self.state = "menu"
self.grid = make_level(1)
self.tanks = []
self.bullets = []
self.particles = []
self.powerups = []
...
def update(self, keys):
for player in self.players:
if not player.dead:
player.update(self, keys)
for bullet in self.bullets:
bullet.update(self)
...
Game 的 update 方法里充满了"告诉对象去做某事"的代码,而不是"亲自计算每一件事"。它告诉玩家更新、告诉子弹更新、告诉敌人更新。每个对象自己决定具体怎么做。这种分工让 Game 保持简洁,也让单个对象的修改不会波及整个项目。
封装、继承、多态在代码中的具体体现
封装:数据和行为绑在一起
Tank 的 rect 是一个只读属性,它根据坦克当前坐标动态计算碰撞矩形。外部代码不需要知道 size 是 30,也不需要自己算左上角坐标,直接访问 tank.rect 就行。
@property
def rect(self):
return pygame.Rect(
int(self.x – self.size / 2),
int(self.y – self.size / 2),
self.size,
self.size
)
如果以后坦克大小要随等级变化,只需要改 size 的初始化逻辑,rect 会自动适应。外部代码完全不需要修改。
继承:没有显式继承,但有复用思想
严格来说,这个版本没有使用 class A(B) 这样的显式继承。但 Tank 同时服务玩家和敌人,本质上是一种"通过参数实现复用"的设计。如果后续要扩展,可以很容易地改成继承结构:
class Tank:
# 公共属性和方法
...
class PlayerTank(Tank):
# 玩家特有的输入处理
...
class EnemyTank(Tank):
# 敌人特有的 AI
...
目前的实现已经足够简洁,所以作者选择了参数化区分。这提醒我们:继承是工具,不是目的。能简单实现的,就不必强行使用继承。
多态:统一接口,不同实现
游戏里的 Tank、Bullet、Particle、PowerUp 都有 update 和 draw 方法。虽然每个类内部的实现完全不同,但 Game 可以用同一种方式调用它们。
for obj in self.bullets + self.particles + self.powerups:
obj.update(self)
obj.draw(screen, offset)
这种"统一接口"的能力是面向对象最实用的特性之一。它让新增对象类型变得容易:只要新类实现了 update 和 draw,就能无缝加入游戏循环。
从过程式到面向对象
想象一个没有类的版本:所有坦克数据用字典或列表保存,移动逻辑、射击逻辑、绘制逻辑全部写在 main 函数里。新增一个敌人,就要新增一组全局变量;修改子弹行为,可能要同时改动三四个地方。
面向对象版本则把每个角色封装成类,main 函数只剩下最高层的游戏循环:
def main():
game = Game()
running = True
while running:
for event in pygame.event.get():
# 处理退出、按键事件
...
game.update(pygame.key.get_pressed())
game.draw()
pygame.display.flip()
clock.tick(FPS)
main 不再关心坦克怎么移动、子弹怎么反弹,它只负责三件事:读取事件、推进游戏状态、刷新画面。具体逻辑都交给了各个类。这种层次分明的结构,正是面向对象编程带来的主要收益。
给初学者的练习建议
读完源码后,最好的学习方式不是再看一遍,而是动手改。下面几个练习由浅入深,能帮你巩固面向对象思维:
小提示:改代码前先画一张类图,标清楚你要动的类和其他类的关系。这样不容易改出 Bug。
总结
一份 500 行的小游戏源码,已经足以展示面向对象编程的核心价值。Tank、Bullet、Particle、PowerUp、Game 五个类各司其职,通过封装隐藏细节,通过统一接口实现协作。理解了这个结构,你再去看更大的项目,会发现它们本质上也遵循同样的组织方式。
如果你正在学习 Python 面向对象,建议把这份代码下载下来跑一跑、改一改。理论知识只有在代码里落地之后,才真正属于你。



