欢迎光临
我们一直在努力

【一个小游戏教你拿捏面向对象】

用《坦克大战》学透 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 不再关心坦克怎么移动、子弹怎么反弹,它只负责三件事:读取事件、推进游戏状态、刷新画面。具体逻辑都交给了各个类。这种层次分明的结构,正是面向对象编程带来的主要收益。

给初学者的练习建议

读完源码后,最好的学习方式不是再看一遍,而是动手改。下面几个练习由浅入深,能帮你巩固面向对象思维:

  • 给 Tank 增加一个 armor 属性,受到攻击时先扣护甲再扣血。
  • 新建一个 Missile 类,让它继承自 Bullet,飞行更慢但威力更大。
  • 为 PowerUp 增加一种新道具,例如"护盾",让玩家短时间内无敌。
  • 把 Tank.update 中的玩家输入和敌人 AI 拆成两个子类 PlayerTank 和 EnemyTank。
  • 给 Game 增加一个 save 和 load 方法,把当前关卡和分数存到文件里。
  • 小提示:改代码前先画一张类图,标清楚你要动的类和其他类的关系。这样不容易改出 Bug。

    总结

    一份 500 行的小游戏源码,已经足以展示面向对象编程的核心价值。Tank、Bullet、Particle、PowerUp、Game 五个类各司其职,通过封装隐藏细节,通过统一接口实现协作。理解了这个结构,你再去看更大的项目,会发现它们本质上也遵循同样的组织方式。

    如果你正在学习 Python 面向对象,建议把这份代码下载下来跑一跑、改一改。理论知识只有在代码里落地之后,才真正属于你。


    赞(0)
    未经允许不得转载:171主机测评 » 【一个小游戏教你拿捏面向对象】
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址