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

发布时间:2026/8/16 5:20:55
【一个小游戏教你拿捏面向对象】 用《坦克大战》学透 Python 面向对象编程从一份 500 行的 pygame 小游戏源码看懂类、封装、继承与多态很多初学者在学完 Python 基础语法后面对面向对象这四个字总觉得抽象。类是什么对象又是什么为什么同样的功能非要用类来写这篇文章用一个完整的《坦克大战》小游戏源码作为例子把面向对象编程拆成看得见、改得动的代码块。为什么选游戏作为入门案例游戏天然包含多个独立角色玩家、敌人、子弹、道具、地图格子。每个角色有自己的状态和行为这正好对应面向对象里的对象。如果我们把所有逻辑都堆在一个大函数里随着功能增加代码会迅速失控。而把这些角色抽象成类每个类只负责自己的逻辑代码就会清晰很多。本文分析的源码是一个单文件 pygame 实现的《坦克大战》支持单人/双人合作、多关卡、敌人 AI、道具掉落和粒子特效。你可以在读完之后直接运行它也可以把它当成改造练习加一种新坦克、换一种新子弹、设计一个新道具。项目运行与整体结构运行这款游戏只需要两步安装 pygame然后执行 Python 文件。# 安装依赖pipinstallpygame# 运行游戏python 坦克大战.py源码虽然只有 500 多行但已经包含了一个完整游戏所需的核心模块。下面这张表列出了主要类及其职责。类名职责对应游戏元素Tank管理位置、方向、生命值、移动、射击、碰撞检测玩家坦克和敌方坦克Bullet处理子弹飞行、反弹、命中判定、墙体破坏坦克发射的炮弹Particle管理粒子位置、速度、生命周期和绘制爆炸、击中特效PowerUp处理道具出现、被拾取后的效果回血、冰冻、急速射击Game控制关卡流程、生成敌人、更新全部对象、绘制画面整个游戏世界面向对象的三根支柱在深入源码之前先快速回顾三个核心概念。它们不是 Python 独有的而是所有面向对象语言共通的设计思想。封装把数据属性和操作数据的方法打包到一个类里外部不需要知道内部细节。继承子类可以复用父类的属性和方法同时扩展自己的特性。多态不同类型的对象可以以相同的方式被调用表现出各自的行为。这款坦克大战的代码把这三种思想都用到了。接下来我们逐个类看它们是如何落地的。类图总览下图展示了游戏中几个核心类的关系。箭头表示依赖例如Game管理多个Tank和Bullet实例而Bullet在被创建时需要知道它的发射者。shoot11111*****Tankx, ydirectionhealthteammove(direction, game)shoot(game)draw(surface, offset)Bulletx, ydirectionownerspeedupdate(game)draw(surface, offset)Particlex, yvx, vylifeupdate()draw(surface)PowerUpx, ykindlifeupdate(game)draw(surface, offset)Gamegridtanksbulletsparticlespowerupsupdate(keys)draw()图 1《坦克大战》核心类关系图Tank 类把坦克抽象成一个对象Tank是游戏里最核心的类。它同时被玩家和敌人复用通过team和player_number区分身份。这种设计避免了写两套几乎一样的代码。classTank:def__init__(self,x,y,team,tier1,player_number0):self.x,self.yx,y self.teamteam self.tiertier self.player_numberplayer_number self.direction0ifteamplayerelse2self.size30self.speed3.0ifteamplayerelse1.65tier*.12self.health2ifteamplayerelsetier self.cooldown0self.deadFalse构造函数__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.yx,y self.directiondirection self.ownerowner self.speed7.4ifowner.teamplayerelse5.4self.bounces0self.deadFalsedefupdate(self,game):# 移动、碰撞、反弹、命中的完整逻辑...把子弹单独抽象成类的好处是Game不需要关心每一颗子弹怎么飞。它只需要维护一个子弹列表每帧调用每颗子弹的update方法再清理掉已经死亡的子弹。Particle 与 PowerUp小而专一的类Particle负责爆炸和击中特效。每个粒子有自己的位置、速度、颜色和剩余生命周期。它只做两件事更新自己的状态把自己画出来。classParticle:defupdate(self):self.xself.vx self.yself.vy self.life-1defdraw(self,surface):radiusmax(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.statemenuself.gridmake_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.bulletsself.particlesself.powerups:obj.update(self)obj.draw(screen,offset)这种统一接口的能力是面向对象最实用的特性之一。它让新增对象类型变得容易只要新类实现了update和draw就能无缝加入游戏循环。从过程式到面向对象想象一个没有类的版本所有坦克数据用字典或列表保存移动逻辑、射击逻辑、绘制逻辑全部写在main函数里。新增一个敌人就要新增一组全局变量修改子弹行为可能要同时改动三四个地方。面向对象版本则把每个角色封装成类main函数只剩下最高层的游戏循环defmain():gameGame()runningTruewhilerunning: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 面向对象建议把这份代码下载下来跑一跑、改一改。理论知识只有在代码里落地之后才真正属于你。