【一个小游戏教你拿捏面向对象】
用《坦克大战》学透 Python 面向对象编程
从一份 500 行的 pygame 小游戏源码,看懂类、封装、继承与多态
很多初学者在学完 Python 基础语法后,面对"面向对象"这四个字总觉得抽象。类是什么?对象又是什么?为什么同样的功能,非要用类来写?这篇文章用一个完整的《坦克大战》小游戏源码作为例子,把面向对象编程拆成看得见、改得动的代码块。
为什么选游戏作为入门案例
游戏天然包含多个独立角色:玩家、敌人、子弹、道具、地图格子。每个角色有自己的状态和行为,这正好对应面向对象里的"对象"。如果我们把所有逻辑都堆在一个大函数里,随着功能增加,代码会迅速失控。而把这些角色抽象成类,每个类只负责自己的逻辑,代码就会清晰很多。
本文分析的源码是一个单文件 pygame 实现的《坦克大战》,支持单人/双人合作、多关卡、敌人 AI、道具掉落和粒子特效。你可以在读完之后直接运行它,也可以把它当成改造练习:加一种新坦克、换一种新子弹、设计一个新道具。
项目运行与整体结构
运行这款游戏只需要两步:安装 pygame,然后执行 Python 文件。
# 安装依赖pipinstallpygame# 运行游戏python 坦克大战.py源码虽然只有 500 多行,但已经包含了一个完整游戏所需的核心模块。下面这张表列出了主要类及其职责。
| 类名 | 职责 | 对应游戏元素 |
|---|---|---|
Tank | 管理位置、方向、生命值、移动、射击、碰撞检测 | 玩家坦克和敌方坦克 |
Bullet | 处理子弹飞行、反弹、命中判定、墙体破坏 | 坦克发射的炮弹 |
Particle | 管理粒子位置、速度、生命周期和绘制 | 爆炸、击中特效 |
PowerUp | 处理道具出现、被拾取后的效果 | 回血、冰冻、急速射击 |
Game | 控制关卡流程、生成敌人、更新全部对象、绘制画面 | 整个游戏世界 |
面向对象的三根支柱
在深入源码之前,先快速回顾三个核心概念。它们不是 Python 独有的,而是所有面向对象语言共通的设计思想。
- 封装:把数据(属性)和操作数据的方法打包到一个类里,外部不需要知道内部细节。
- 继承:子类可以复用父类的属性和方法,同时扩展自己的特性。
- 多态:不同类型的对象可以以相同的方式被调用,表现出各自的行为。
这款坦克大战的代码把这三种思想都用到了。接下来我们逐个类看它们是如何落地的。
类图总览
下图展示了游戏中几个核心类的关系。箭头表示依赖:例如Game管理多个Tank和Bullet实例,而Bullet在被创建时需要知道它的发射者。
图 1:《坦克大战》核心类关系图
Tank 类:把"坦克"抽象成一个对象
Tank是游戏里最核心的类。它同时被玩家和敌人复用,通过team和player_number区分身份。这种设计避免了写两套几乎一样的代码。
classTank: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=0ifteam=="player"else2self.size=30self.speed=3.0ifteam=="player"else1.65+tier*.12self.health=2ifteam=="player"elsetier self.cooldown=0self.dead=False构造函数__init__把坦克的所有状态都集中初始化:坐标、阵营、等级、方向、速度、生命值。这就是封装:坦克知道自己长什么样、能跑多快、还剩多少血,外部代码不需要手动维护这些变量。
Tank还提供了统一的行为接口:
move(direction, game):按方向移动,并处理碰撞。shoot(game):创建一颗子弹,并进入冷却。update(game, keys):每帧更新状态,包括玩家输入或敌人 AI。draw(surface, offset):把自己画到屏幕上。
注意update方法里的分支:玩家坦克读取键盘输入,敌人坦克执行 AI 逻辑。虽然内部行为不同,但对外接口完全一样。这正是多态的体现:调用方只需要说"更新",不同类型的坦克会各自做出正确的反应。
Bullet 类:独立的生命周期管理
子弹和坦克是两个完全不同的概念。子弹有飞行方向、速度、反弹次数和所有者;它会在碰到墙时反弹,碰到坦克时造成伤害,反弹超过三次后消失。
classBullet:def__init__(self,x,y,direction,owner):self.x,self.y=x,y self.direction=direction self.owner=owner self.speed=7.4ifowner.team=="player"else5.4self.bounces=0self.dead=Falsedefupdate(self,game):# 移动、碰撞、反弹、命中的完整逻辑...把子弹单独抽象成类的好处是,Game不需要关心每一颗子弹怎么飞。它只需要维护一个子弹列表,每帧调用每颗子弹的update方法,再清理掉已经死亡的子弹。
Particle 与 PowerUp:小而专一的类
Particle负责爆炸和击中特效。每个粒子有自己的位置、速度、颜色和剩余生命周期。它只做两件事:更新自己的状态,把自己画出来。
classParticle:defupdate(self):self.x+=self.vx self.y+=self.vy self.life-=1defdraw(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类不直接代表某个可见物体,而是负责把所有对象组织起来。它维护地图、玩家、敌人、子弹、粒子和道具的集合,并在每一帧里协调它们的更新和绘制。
classGame:def__init__(self):self.state="menu"self.grid=make_level(1)self.tanks=[]self.bullets=[]self.particles=[]self.powerups=[]...defupdate(self,keys):forplayerinself.players:ifnotplayer.dead:player.update(self,keys)forbulletinself.bullets:bullet.update(self)...Game的update方法里充满了"告诉对象去做某事"的代码,而不是"亲自计算每一件事"。它告诉玩家更新、告诉子弹更新、告诉敌人更新。每个对象自己决定具体怎么做。这种分工让Game保持简洁,也让单个对象的修改不会波及整个项目。
封装、继承、多态在代码中的具体体现
封装:数据和行为绑在一起
Tank的rect是一个只读属性,它根据坦克当前坐标动态计算碰撞矩形。外部代码不需要知道size是 30,也不需要自己算左上角坐标,直接访问tank.rect就行。
@propertydefrect(self):returnpygame.Rect(int(self.x-self.size/2),int(self.y-self.size/2),self.size,self.size)如果以后坦克大小要随等级变化,只需要改size的初始化逻辑,rect会自动适应。外部代码完全不需要修改。
继承:没有显式继承,但有复用思想
严格来说,这个版本没有使用class A(B)这样的显式继承。但Tank同时服务玩家和敌人,本质上是一种"通过参数实现复用"的设计。如果后续要扩展,可以很容易地改成继承结构:
classTank:# 公共属性和方法...classPlayerTank(Tank):# 玩家特有的输入处理...classEnemyTank(Tank):# 敌人特有的 AI...目前的实现已经足够简洁,所以作者选择了参数化区分。这提醒我们:继承是工具,不是目的。能简单实现的,就不必强行使用继承。
多态:统一接口,不同实现
游戏里的Tank、Bullet、Particle、PowerUp都有update和draw方法。虽然每个类内部的实现完全不同,但Game可以用同一种方式调用它们。
forobjinself.bullets+self.particles+self.powerups:obj.update(self)obj.draw(screen,offset)这种"统一接口"的能力是面向对象最实用的特性之一。它让新增对象类型变得容易:只要新类实现了update和draw,就能无缝加入游戏循环。
从过程式到面向对象
想象一个没有类的版本:所有坦克数据用字典或列表保存,移动逻辑、射击逻辑、绘制逻辑全部写在main函数里。新增一个敌人,就要新增一组全局变量;修改子弹行为,可能要同时改动三四个地方。
面向对象版本则把每个角色封装成类,main函数只剩下最高层的游戏循环:
defmain():game=Game()running=Truewhilerunning:foreventinpygame.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 面向对象,建议把这份代码下载下来跑一跑、改一改。理论知识只有在代码里落地之后,才真正属于你。