【一个小游戏教你拿捏面向对象】
2026/8/16 5:20:46 网站建设 项目流程

用《坦克大战》学透 Python 面向对象编程

从一份 500 行的 pygame 小游戏源码,看懂类、封装、继承与多态

很多初学者在学完 Python 基础语法后,面对"面向对象"这四个字总觉得抽象。类是什么?对象又是什么?为什么同样的功能,非要用类来写?这篇文章用一个完整的《坦克大战》小游戏源码作为例子,把面向对象编程拆成看得见、改得动的代码块。

为什么选游戏作为入门案例

游戏天然包含多个独立角色:玩家、敌人、子弹、道具、地图格子。每个角色有自己的状态和行为,这正好对应面向对象里的"对象"。如果我们把所有逻辑都堆在一个大函数里,随着功能增加,代码会迅速失控。而把这些角色抽象成类,每个类只负责自己的逻辑,代码就会清晰很多。

本文分析的源码是一个单文件 pygame 实现的《坦克大战》,支持单人/双人合作、多关卡、敌人 AI、道具掉落和粒子特效。你可以在读完之后直接运行它,也可以把它当成改造练习:加一种新坦克、换一种新子弹、设计一个新道具。

项目运行与整体结构

运行这款游戏只需要两步:安装 pygame,然后执行 Python 文件。

# 安装依赖pipinstallpygame# 运行游戏python 坦克大战.py

源码虽然只有 500 多行,但已经包含了一个完整游戏所需的核心模块。下面这张表列出了主要类及其职责。

类名职责对应游戏元素
Tank管理位置、方向、生命值、移动、射击、碰撞检测玩家坦克和敌方坦克
Bullet处理子弹飞行、反弹、命中判定、墙体破坏坦克发射的炮弹
Particle管理粒子位置、速度、生命周期和绘制爆炸、击中特效
PowerUp处理道具出现、被拾取后的效果回血、冰冻、急速射击
Game控制关卡流程、生成敌人、更新全部对象、绘制画面整个游戏世界

面向对象的三根支柱

在深入源码之前,先快速回顾三个核心概念。它们不是 Python 独有的,而是所有面向对象语言共通的设计思想。

  • 封装:把数据(属性)和操作数据的方法打包到一个类里,外部不需要知道内部细节。
  • 继承:子类可以复用父类的属性和方法,同时扩展自己的特性。
  • 多态:不同类型的对象可以以相同的方式被调用,表现出各自的行为。

这款坦克大战的代码把这三种思想都用到了。接下来我们逐个类看它们是如何落地的。

类图总览

下图展示了游戏中几个核心类的关系。箭头表示依赖:例如Game管理多个TankBullet实例,而Bullet在被创建时需要知道它的发射者。

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是游戏里最核心的类。它同时被玩家和敌人复用,通过teamplayer_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)...

Gameupdate方法里充满了"告诉对象去做某事"的代码,而不是"亲自计算每一件事"。它告诉玩家更新、告诉子弹更新、告诉敌人更新。每个对象自己决定具体怎么做。这种分工让Game保持简洁,也让单个对象的修改不会波及整个项目。

封装、继承、多态在代码中的具体体现

封装:数据和行为绑在一起

Tankrect是一个只读属性,它根据坦克当前坐标动态计算碰撞矩形。外部代码不需要知道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...

目前的实现已经足够简洁,所以作者选择了参数化区分。这提醒我们:继承是工具,不是目的。能简单实现的,就不必强行使用继承。

多态:统一接口,不同实现

游戏里的TankBulletParticlePowerUp都有updatedraw方法。虽然每个类内部的实现完全不同,但Game可以用同一种方式调用它们。

forobjinself.bullets+self.particles+self.powerups:obj.update(self)obj.draw(screen,offset)

这种"统一接口"的能力是面向对象最实用的特性之一。它让新增对象类型变得容易:只要新类实现了updatedraw,就能无缝加入游戏循环。

从过程式到面向对象

想象一个没有类的版本:所有坦克数据用字典或列表保存,移动逻辑、射击逻辑、绘制逻辑全部写在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不再关心坦克怎么移动、子弹怎么反弹,它只负责三件事:读取事件、推进游戏状态、刷新画面。具体逻辑都交给了各个类。这种层次分明的结构,正是面向对象编程带来的主要收益。

给初学者的练习建议

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

  1. Tank增加一个armor属性,受到攻击时先扣护甲再扣血。
  2. 新建一个Missile类,让它继承自Bullet,飞行更慢但威力更大。
  3. PowerUp增加一种新道具,例如"护盾",让玩家短时间内无敌。
  4. Tank.update中的玩家输入和敌人 AI 拆成两个子类PlayerTankEnemyTank
  5. Game增加一个saveload方法,把当前关卡和分数存到文件里。

小提示:改代码前先画一张类图,标清楚你要动的类和其他类的关系。这样不容易改出 Bug。

总结

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

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


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询