朋友们,当你在 B 站或短视频平台刷到那些“生物打斗模拟器”时,是不是很羡慕?用鼠标画一个怪物,再设定它跟一只猫打架,看着它们按照程序逻辑互相追逐、攻击,特别有意思。其实,这种“自定义生物对战”玩法并不神秘,核心就是对象属性建模、简单的 AI 行为决策和碰撞检测。
今天这篇教程,我就带大家手写一个“卡通猫 vs 自创生物”的桌面小游戏。你可以用键盘操控卡通猫,然后用鼠标在屏幕上画出任意颜色的“自创生物”,它们会自动攻击你,而你也要想办法消灭它们。项目纯 Python 实现,不依赖复杂的游戏引擎,重点讲清楚“自创生物”是怎么被定义出来的,以及双方对战的逻辑到底怎么设计。
无论你是刚开始学 Pygame 的萌新,还是想找一个“图形化 + 面向对象 + 游戏循环”综合练手项目的开发者,这篇实战笔记都能给你一套完整的代码思路和排错方案。文章里所有代码都可以直接复制运行,环境要求也写得很清楚。
1. 项目背景与核心概念
1.1 这个项目到底做了什么
简单来说,这个项目是一个基于Pygame的 2D 互动小游戏。启动程序后,你看到的画面大概是这样:
- 屏幕中央有一只像素风格的“卡通猫”,它由圆形身体、三角形耳朵、眼睛和胡须组成,全部用 Pygame 的绘图函数画出来,不需要任何图片素材。
- 鼠标在窗口任意位置点击,就会生成一只“自创生物”。这种生物长得很随意,可能是带触手的圆球,也可能是带尖刺的方块。重点是:它的颜色、大小、攻击力、移动速度都是随机生成的。
- 自创生物会主动向卡通猫移动,碰到卡通猫就会造成伤害。卡通猫可以按方向键移动躲避,按空格键释放“爪击”攻击,消灭靠近的生物。
你可以把它理解成一个“简化版幸存者类游戏”,只不过这里没有关卡、没有分数系统,而是把重心放在“如何定义和生成自创生物”的机制上。读者可以自己扩展:给生物加不同的技能、加血量条、加波次生成逻辑等等。
1.2 为什么值得动手做一遍
虽然市面上已经有很多成熟的游戏引擎(如 Unity、Godot),但对于学习 Python、学习面向对象设计的人来说,Pygame 是一个很棒的练习平台。它足够底层,让你理解游戏循环、事件处理、碰撞检测这些基础概念;又不会太底层,绘图和窗口操作已经封装得很友好。
这个实战项目尤其适合下面几类朋友:
- 刚学完 Python 基础语法,想找一个“练手项目”巩固类与对象的用法。
- 想学习 Pygame 但不知道从哪里开始,需要一个小而完整的项目模板。
- 对游戏 AI 感兴趣,想看看“敌人追击玩家”的简单算法怎么写。
- 做少儿编程或培训教学,需要一个视觉效果有趣、参与感强的案例。
1.3 几个容易混淆的概念
先区分几个概念,避免后面看代码时迷路。
精灵(Sprite) vs 普通对象在 Pygame 中,精灵可以简单理解为“有图片或外表、可以移动、可以绘制到窗口上的对象”。我们当然可以用普通 Python 类来定义卡通猫和自创生物,但使用pygame.sprite.Sprite基类,可以更方便地利用 Pygame 提供的精灵组(Group)功能,比如快速遍历所有生物、统一更新位置、统一绘制。本项目的代码就使用继承 Sprite 的方式。
碰撞检测 vs 攻击判定碰撞检测是指“两个物体边界是否重叠”,比如卡通猫和自创生物碰在一起。攻击判定是指“攻击区域与目标区域是否重叠”,比如卡通猫释放爪击时,我们要检查爪击的圆形范围是否覆盖了某个生物。两者本质都是几何关系判断,只是语义不同。项目里会分别实现。
面向对象设计 vs 面向过程实现如果把所有代码都写在主循环里,代码会非常长且难以维护。更合理的做法是定义Cat、Creature、ClawAttack三个类,分别负责各自的行为。这样后期想加一个新技能,只要新写一个类,再在主循环中调用就行。
2. 环境准备与版本说明
2.1 安装 Python
本文所有代码基于 Python 3,示例环境为 Windows 11,但 macOS 和 Linux 也兼容。请先确认你的电脑已经安装 Python 3.8 或更高版本:
python --version如果提示找不到命令,可以尝试:
python3 --version没有安装 Python 的读者,去官网下载稳定版安装包即可。安装时记得勾选“Add Python to PATH”。
2.2 安装 Pygame
项目依赖只需要一个第三方库:pygame。建议创建一个虚拟环境,防止污染系统 Python:
# 创建虚拟环境(Windows) python -m venv venv venv\Scripts\activate # 创建虚拟环境(macOS / Linux) python3 -m venv venv source venv/bin/activate然后安装 Pygame:
pip install pygame安装完成后可以验证一下:
python -c "import pygame; print(pygame.version.ver)"我这里以 Pygame 2.x 版本为例,2.0 以上都可以兼容本文代码。如果你使用的是 1.9.x,部分坐标 API 会有细微差别,建议升级到 2.x。
2.3 项目文件结构
为了保持清晰,我们把项目拆成三个文件:
cat_vs_creature/ │ ├── main.py # 程序入口,负责游戏主循环 ├── cat.py # 卡通猫类定义 ├── creature.py # 自创生物类定义 └── requirements.txt # 依赖清单如果你只想快速体验,也可以把所有代码写在一个main.py里。不过为了演示工程化组织方式,本文采用拆分文件的做法。
2.4 为什么用 Pygame 而不是其他库
Pygame 的优势在于:
- 基于 SDL 库,跨平台支持好,窗口、鼠标、键盘事件处理稳定。
- 绘图 API 简单,
pygame.draw.circle、pygame.draw.rect就可以完成所有图形绘制,不需要额外素材。 - 精灵组、碰撞检测、音效播放都内置,适合中小型项目。
- 文档和社区资料丰富,踩坑容易解决。
当然,如果你要开发大型 3D 游戏,Pygame 并不是最佳选择。但作为教学和练手项目,它非常合适。
3. 核心代码拆解:自创生物是怎么来的
这一节我们先不看完整代码,而是把最核心的几个知识点单独拎出来讲,后面再串成完整项目。
3.1 数据类:定义生物的属性
“自创生物”最大的特点就是“每个个体都不一样”。我们可以用一个普通类来保存生物的各种属性,并在创建时随机赋值。
# creature.py 中的属性初始化片段 import random class CreatureAttributes: """自创生物属性数据类""" def __init__(self): self.body_color = ( random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) ) self.radius = random.randint(15, 30) self.speed = random.uniform(1.0, 2.5) self.hp = random.randint(1, 3) self.damage = random.randint(1, 2)这里可以看到,每个生物被创建时,颜色、大小、速度、血量、伤害都是随机的。这就是“自创”的含义——用户在屏幕上点击一下,程序就生成一个属性完全随机的敌人。
3.2 追击 AI:敌人怎么朝玩家移动
这是游戏里最核心的逻辑之一。假设生物位置为(bx, by),猫的位置为(cx, cy),要让生物向猫移动,可以计算两者之间的方向向量,然后乘以速度。
import math def move_towards(bx, by, cx, cy, speed): dx = cx - bx dy = cy - by distance = math.hypot(dx, dy) if distance == 0: return bx, by step_x = dx / distance * speed step_y = dy / distance * speed return bx + step_x, by + step_y这种写法的好处是,速度恒定,不会因为距离太近而出现“瞬移”。如果你直接写bx += speed然后也by += speed,那生物斜向移动时会比水平移动更快,不符合预期。
3.3 卡通猫的绘制:纯代码画出一只猫
卡通猫不需要图片,全部用 Pygame 绘图函数绘制。最基本的形状包括:
- 两个三角形耳朵
- 一个圆形身体
- 两只圆眼睛
- 一个粉色小鼻子
- 几根胡须
三角形在 Pygame 中可以用pygame.draw.polygon来画,需要提供三个顶点坐标。为了让人物有“朝向”,我们设定猫的耳朵、胡须跟随移动方向轻微偏移。
3.4 碰撞检测与攻击判定
Pygame 提供了非常方便的碰撞检测方法:
# 检查一个点和圆形是否碰撞 def circle_collide(px, py, circle_x, circle_y, radius): return math.hypot(px - circle_x, py - circle_y) <= radius在两个精灵都有rect属性时,可以直接用:
pygame.sprite.collide_rect(cat, creature)但是,对于圆形对象,使用圆形碰撞判断会更准确。为了简单,我们可以在绘制精灵时,让精灵的rect近似包裹圆形,然后使用 Pygame 的pygame.sprite.collide_circle,它要求精灵对象带有radius属性。
3.5 主循环:游戏是如何跑起来的
所有 Pygame 游戏都有一个主循环(game loop),它的任务是反复执行四件事:
- 处理事件(键盘、鼠标、窗口关闭)。
- 更新游戏状态(移动猫、移动生物、检测碰撞)。
- 绘制画面(画猫、画生物、画攻击特效)。
- 控制帧率(
clock.tick(60)表示每秒最多 60 帧)。
理解了这个循环,后面看main.py就很容易了。
4. 完整实战:一步一步实现“卡通猫 vs 自创生物”
现在,我们把前面拆解的知识点串成一个可以直接运行的项目。按照下面的步骤操作,你可以完整跑起这个游戏。
4.1 创建项目目录与依赖文件
先新建一个文件夹,命名为cat_vs_creature,然后进入该目录。
创建requirements.txt,内容如下:
pygame>=2.0然后在终端安装依赖:
pip install -r requirements.txt4.2 编写精灵基类辅助模块
为了让代码更清晰,我先把生物和猫共用的“自转角度”和“方向向量”等辅助逻辑放在各自类中,不额外写父类。不过为了方便管理,我建议给生物写一个单独的模块。
创建creature.py:
# 文件路径:cat_vs_creature/creature.py import math import random import pygame class Creature(pygame.sprite.Sprite): """自创生物:属性随机生成,自动追踪卡通猫""" def __init__(self, x, y, target_cat): super().__init__() # 随机属性 self.radius = random.randint(15, 30) self.speed = random.uniform(1.0, 2.5) self.max_hp = random.randint(1, 3) self.hp = self.max_hp self.damage = random.randint(1, 2) # 随机身体颜色(确保不是纯黑或纯白,便于观察) self.body_color = ( random.randint(80, 255), random.randint(80, 255), random.randint(80, 255) ) # 随机“生物类型”:1 圆球触手怪,2 尖刺方块怪 self.creature_type = random.choice([1, 2]) # 记录目标对象 self.target = target_cat # 创建图像和矩形 self.image = pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA) self.rect = self.image.get_rect(center=(x, y)) self.radius_attr = self.radius # 初始角度,用于动画抖动 self.angle = random.uniform(0, math.pi * 2) # 绘制初始外观 self._draw_creature() def _draw_creature(self): """根据生物类型绘制不同外观""" self.image.fill((0, 0, 0, 0)) cx = self.radius cy = self.radius if self.creature_type == 1: # 圆球 + 触手 for i in range(8): angle = i * math.pi / 4 tentacle_end_x = cx + math.cos(angle) * (self.radius + 6) tentacle_end_y = cy + math.sin(angle) * (self.radius + 6) pygame.draw.line( self.image, self.body_color, (cx, cy), (tentacle_end_x, tentacle_end_y), max(2, self.radius // 6) ) pygame.draw.circle(self.image, self.body_color, (cx, cy), self.radius) else: # 尖刺方块 rect_size = self.radius * 1.5 pygame.draw.rect( self.image, self.body_color, (cx - rect_size / 2, cy - rect_size / 2, rect_size, rect_size), border_radius=4 ) # 四个方向的尖刺,攻击性更强 pygame.draw.polygon(self.image, self.body_color, [ (cx - rect_size / 2, cy), (cx - rect_size / 2 - 8, cy), (cx - rect_size / 2, cy - 6) ]) pygame.draw.polygon(self.image, self.body_color, [ (cx + rect_size / 2, cy), (cx + rect_size / 2 + 8, cy), (cx + rect_size / 2, cy - 6) ]) # 画眼睛:让生物看起来有攻击性 eye_color = (255, 255, 255) pygame.draw.circle(self.image, eye_color, (cx - 6, cy - 4), 4) pygame.draw.circle(self.image, eye_color, (cx + 6, cy - 4), 4) pygame.draw.circle(self.image, (0, 0, 0), (cx - 6, cy - 4), 2) pygame.draw.circle(self.image, (0, 0, 0), (cx + 6, cy - 4), 2) def update(self, *args, **kwargs): """每帧更新:朝目标移动,并轻微抖动""" if self.target is None: return tx, ty = self.target.rect.center dx = tx - self.rect.centerx dy = ty - self.rect.centery dist = math.hypot(dx, dy) if dist > 0: step_x = dx / dist * self.speed step_y = dy / dist * self.speed self.rect.x += step_x self.rect.y += step_y # 轻微抖动:增加生物感 self.angle += 0.1 offset_x = math.sin(self.angle) * 0.5 offset_y = math.cos(self.angle * 0.8) * 0.5 self.rect.x += offset_x self.rect.y += offset_y def get_hit(self, damage): """受击处理,返回 True 表示生物被消灭""" self.hp -= damage return self.hp <= 0上面的代码有几个地方需要注意:
Creature继承自pygame.sprite.Sprite,因此可以放到精灵组中统一管理。self.image使用了SRCALPHA透明通道,这样画出来的图形可以是不规则形状,不会带着黑色背景框。_draw_creature方法根据creature_type决定画触手怪还是尖刺怪,这样每次点击生成的生物不仅颜色不一样,形状也不太一样。update方法里除了追踪目标,还给了一个轻微抖动效果,让生物看起来更“活”。
4.3 编写卡通猫模块
创建cat.py:
# 文件路径:cat_vs_creature/cat.py import math import pygame class Cat(pygame.sprite.Sprite): """卡通猫:玩家控制,可以移动和释放爪击""" def __init__(self, x, y): super().__init__() self.radius = 25 self.speed = 4 self.max_hp = 10 self.hp = self.max_hp self.attack_cooldown = 0 # 猫的基础颜色 self.body_color = (245, 176, 65) self.ear_color = (230, 126, 34) self.nose_color = (255, 105, 180) self.image = pygame.Surface((self.radius * 2 + 20, self.radius * 2 + 20), pygame.SRCALPHA) self.rect = self.image.get_rect(center=(x, y)) self.direction = 1 # 1 表示朝向右侧,-1 表示朝向左侧 # 攻击特效对象(单独管理) self.attack_effects = pygame.sprite.Group() self._draw_cat() def _draw_cat(self): """用绘图函数画出卡通猫""" self.image.fill((0, 0, 0, 0)) cx = self.rect.width // 2 cy = self.rect.height // 2 # 耳朵(两个三角形) left_ear_base = (cx - 18, cy - 5) left_ear_top = (cx - 28, cy - 25) left_ear_back = (cx - 5, cy - 18) pygame.draw.polygon(self.image, self.ear_color, [left_ear_base, left_ear_top, left_ear_back]) right_ear_base = (cx + 18, cy - 5) right_ear_top = (cx + 28, cy - 25) right_ear_back = (cx + 5, cy - 18) pygame.draw.polygon(self.image, self.ear_color, [right_ear_base, right_ear_top, right_ear_back]) # 身体(圆) pygame.draw.circle(self.image, self.body_color, (cx, cy), self.radius) # 肚子(浅色) pygame.draw.circle(self.image, (255, 224, 178), (cx, cy + 6), self.radius - 8) # 眼睛 eye_y = cy - 6 eye_offset = 8 if self.direction == 1: pygame.draw.circle(self.image, (255, 255, 255), (cx - eye_offset, eye_y), 7) pygame.draw.circle(self.image, (0, 0, 0), (cx - eye_offset + 2, eye_y), 3) pygame.draw.circle(self.image, (255, 255, 255), (cx + eye_offset, eye_y), 7) pygame.draw.circle(self.image, (0, 0, 0), (cx + eye_offset + 2, eye_y), 3) else: pygame.draw.circle(self.image, (255, 255, 255), (cx - eye_offset, eye_y), 7) pygame.draw.circle(self.image, (0, 0, 0), (cx - eye_offset - 2, eye_y), 3) pygame.draw.circle(self.image, (255, 255, 255), (cx + eye_offset, eye_y), 7) pygame.draw.circle(self.image, (0, 0, 0), (cx + eye_offset - 2, eye_y), 3) # 鼻子 pygame.draw.circle(self.image, self.nose_color, (cx, cy + 4), 4) # 胡须 pygame.draw.line(self.image, (0, 0, 0), (cx - 12, cy + 1), (cx - 28, cy - 4), 2) pygame.draw.line(self.image, (0, 0, 0), (cx - 12, cy + 6), (cx - 28, cy + 8), 2) pygame.draw.line(self.image, (0, 0, 0), (cx + 12, cy + 1), (cx + 28, cy - 4), 2) pygame.draw.line(self.image, (0, 0, 0), (cx + 12, cy + 6), (cx + 28, cy + 8), 2) def update(self, keys_pressed): """根据键盘输入移动猫的位置""" old_x, old_y = self.rect.x, self.rect.y if keys_pressed[pygame.K_LEFT] or keys_pressed[pygame.K_a]: self.rect.x -= self.speed self.direction = -1 if keys_pressed[pygame.K_RIGHT] or keys_pressed[pygame.K_d]: self.rect.x += self.speed self.direction = 1 if keys_pressed[pygame.K_UP] or keys_pressed[pygame.K_w]: self.rect.y -= self.speed if keys_pressed[pygame.K_DOWN] or keys_pressed[pygame.K_s]: self.rect.y += self.speed # 防止猫移出屏幕边缘 screen_width = pygame.display.get_surface().get_width() screen_height = pygame.display.get_surface().get_height() self.rect.clamp_ip(pygame.Rect(0, 0, screen_width, screen_height)) # 如果位置变化了,重新绘制方向 if self.rect.x != old_x: self._draw_cat() # 攻击冷却时间递减 if self.attack_cooldown > 0: self.attack_cooldown -= 1 def attack(self): """释放爪击:在猫前方生成一个攻击特效""" if self.attack_cooldown > 0: return None from claw_attack import ClawAttack claw = ClawAttack(self.rect.centerx + self.direction * 35, self.rect.centery - 5, self.direction) self.attack_effects.add(claw) self.attack_cooldown = 15 return claw def get_hit(self, damage): """被生物攻击""" self.hp -= damage return self.hp <= 0这里有一个需要注意的地方:attack方法里引入了ClawAttack,这是我们要在下一小节创建的类。它用于表现猫的“爪击”攻击特效,同时也用来检测攻击是否命中生物。
为了让猫移动时嘴唇胡须朝向正确,我们在update中检测rect.x是否变化,并重新调用_draw_cat。这是一种简单的“朝向刷新”策略,虽然不算高效,但在这个小项目中完全够用。
4.4 编写爪击攻击特效
创建claw_attack.py:
# 文件路径:cat_vs_creature/claw_attack.py import pygame class ClawAttack(pygame.sprite.Sprite): """爪击攻击特效:横向飞出的三道爪痕""" def __init__(self, x, y, direction): super().__init__() self.direction = direction # 1 向右,-1 向左 self.life = 10 # 特效持续帧数 self.image = pygame.Surface((70, 60), pygame.SRCALPHA) self.rect = self.image.get_rect(center=(x, y)) self._draw_claws() def _draw_claws(self): """绘制三道平行的爪痕""" self.image.fill((0, 0, 0, 0)) claw_color = (200, 200, 200) for i, offset_y in enumerate([-15, 0, 15]): y = 30 + offset_y if self.direction == 1: start_x = 10 end_x = 55 else: start_x = 55 end_x = 10 pygame.draw.line(self.image, claw_color, (start_x, y - 5), (end_x, y), 3) pygame.draw.line(self.image, claw_color, (start_x, y + 5), (end_x, y), 3) def update(self, *args, **kwargs): """每帧移动一小段距离,然后减少生命值""" self.rect.x += self.direction * 8 self.life -= 1 if self.life <= 0: self.kill()这里的攻击特效比较简单,就是三道平行横线。你可以根据自己的喜好改成圆弧、利爪形状,甚至给图片加上透明度渐变效果。
4.5 编写主程序入口
最后是main.py,把上面的类全部串起来。
# 文件路径:cat_vs_creature/main.py import pygame import random from cat import Cat from creature import Creature # 初始化 Pygame pygame.init() # 窗口设置 WINDOW_WIDTH = 800 WINDOW_HEIGHT = 600 BACKGROUND_COLOR = (30, 30, 40) screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption("卡通猫 vs 自创生物") clock = pygame.time.Clock() # 创建卡通猫 cat = Cat(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2) # 精灵组 all_sprites = pygame.sprite.Group() creatures = pygame.sprite.Group() all_sprites.add(cat) # 分数统计(不算正式功能,只用于显示消灭数量) kill_count = 0 font = pygame.font.Font(None, 36) def spawn_creature(mouse_x, mouse_y): """在鼠标位置生成一个自创生物""" creature = Creature(mouse_x, mouse_y, cat) creatures.add(creature) all_sprites.add(creature) def draw_game_over(surface, text): """游戏结束时绘制文本""" overlay = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 180)) surface.blit(overlay, (0, 0)) over_font = pygame.font.Font(None, 64) over_text = over_font.render("游戏结束", True, (255, 80, 80)) text_rect = over_text.get_rect(center=(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2 - 30)) surface.blit(over_text, text_rect) tip_text = font.render("按 R 重新开始,按 ESC 退出", True, (255, 255, 255)) tip_rect = tip_text.get_rect(center=(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2 + 30)) surface.blit(tip_text, tip_rect) # 游戏主循环 running = True game_over = False while running: # 事件处理 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False if game_over and event.key == pygame.K_r: # 重新开始 all_sprites.empty() creatures.empty() cat = Cat(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2) all_sprites.add(cat) kill_count = 0 game_over = False elif event.type == pygame.MOUSEBUTTONDOWN: if not game_over and event.button == 1: mouse_x, mouse_y = event.pos spawn_creature(mouse_x, mouse_y) if not game_over: # 玩家操作 keys = pygame.key.get_pressed() cat.update(keys) # 释放爪击 if keys[pygame.K_SPACE]: cat.attack() # 更新所有特效 cat.attack_effects.update() # 更新所有生物 creatures.update() # 碰撞检测: # 1. 生物碰到猫 for creature in creatures: if pygame.sprite.collide_circle(cat, creature): is_dead = cat.get_hit(creature.damage) creature.get_hit(99) # 接触伤害并消失 if is_dead: game_over = True # 2. 猫的爪击命中生物 for claw in cat.attack_effects: hit_creatures = pygame.sprite.spritecollide( claw, creatures, False, pygame.sprite.collide_circle ) for creature in hit_creatures: should_remove = creature.get_hit(1) if should_remove: creatures.remove(creature) all_sprites.remove(creature) kill_count += 1 # 清理死亡生物 for creature in list(creatures): if creature.hp <= 0: creatures.remove(creature) all_sprites.remove(creature) kill_count += 1 # 绘制 screen.fill(BACKGROUND_COLOR) all_sprites.draw(screen) cat.attack_effects.draw(screen) # 绘制血条 # 猫的血条 pygame.draw.rect(screen, (0, 0, 0), (20, 20, 204, 24)) pygame.draw.rect(screen, (255, 80, 80), (22, 22, int(200 * cat.hp / cat.max_hp), 20)) # 击杀数量 count_text = font.render(f"消灭: {kill_count}", True, (255, 255, 255)) screen.blit(count_text, (20, 55)) # 提示文本 tip_text = font.render("鼠标点击生成生物 | 方向键移动 | 空格攻击", True, (200, 200, 200)) tip_rect = tip_text.get_rect(center=(WINDOW_WIDTH // 2, WINDOW_HEIGHT - 30)) screen.blit(tip_text, tip_rect) if game_over: draw_game_over(screen, "游戏结束") pygame.display.flip() clock.tick(60) pygame.quit()4.6 运行与验证
完成以上步骤后,在项目目录下运行:
python main.py预期效果:
- 窗口标题为“卡通猫 vs 自创生物”。
- 窗口中央出现一只橙色卡通猫。
- 鼠标左键点击屏幕任意位置,会生成一个颜色、大小、形态随机的生物。
- 生物会缓慢向猫移动,并伴随轻微抖动。
- 用方向键 WASD 控制猫移动,按空格键释放爪击攻击。
- 生物碰到猫会扣除猫的血量,同时生物消失。
- 猫的爪击命中生物时,生物扣除 1 点生命,血量归零则被消灭。
- 猫的血量归零时,画面覆盖黑色半透明层,显示“游戏结束”,按 R 重新开始,按 ESC 退出。
如果你看到猫的血条旁边有一个实时变化的“消灭”数字,说明主循环和碰撞检测都工作正常。
5. 常见问题与排查思路
实际运行过程中,你可能会遇到一些报错或不符合预期的现象。我根据自己的经验,把最常遇到的问题整理在下面。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
运行报错ModuleNotFoundError: No module named 'pygame' | 没有安装 Pygame,或者安装到了别的 Python 环境 | 在项目虚拟环境中执行pip install pygame,再检查是否用python main.py运行 |
| 窗口闪退 | 代码中可能有异常导致主循环提前退出 | 用命令行运行程序,查看具体报错;最常见是导入路径错误,例如claw_attack模块没有被正确导入 |
| 生成的生物是黑色方块 | pygame.Surface没有使用SRCALPHA,导致背景不透明 | 确认Creature.__init__中创建 Surface 时传入了pygame.SRCALPHA |
| 猫移动不流畅或方向不对 | rect.clamp_ip或方向判断逻辑有问题 | 检查Cat.update中old_x与self.rect.x的比较逻辑;同时确认按键检测使用的是pygame.key.get_pressed()而不是事件里的按键 |
| 爪击无法消灭生物 | 碰撞检测使用的collide_circle要求两个精灵都有radius属性,或者rect大小与图形的实际大小一致 | 检查Creature对象是否设置了self.radius_attr,在main.py中给creature.radius = creature.radius_attr;另外确认ClawAttack的rect覆盖了利爪实际绘图范围 |
| 生物重叠在一起,很难分辨 | 多次点击同一个位置会生成多个生物,它们自然重叠 | 可以给生物之间加上简单的排斥逻辑,或者在生成时判断鼠标位置附近是否已有生物 |
| 游戏结束后无法重新开始 | game_over状态没有正确处理所有精灵组和引用 | 检查重新开始逻辑中是否重新创建了cat,同时清空了all_sprites和creatures |
| 程序占用 CPU 过高 | 主循环没有限制帧率 | 确认循环末尾有clock.tick(60) |
如果遇到pygame.display.get_surface()返回None的报错,通常是因为在Cat类初始化时,Pygame 窗口还没有创建。解决办法是在创建窗口之后再实例化 Cat 对象。我们在main.py中已经按这个顺序编写,如果你之前在别的模块实例化了 Cat,注意顺序。
6. 最佳实践与工程建议
代码跑通只是第一步,想要真正把这个项目当作练习或教学案例,下面这些建议值得参考。
6.1 面向对象设计:不要让主循环膨胀
你在网上会看到很多 Pygame 教程,把精灵、碰撞检测、背景绘制全部堆在while running循环里。这样做虽然能快速出效果,但后期加功能非常痛苦。本文的做法是把每个角色和攻击特效封装成独立类:
Cat负责自己的移动、绘制、受击和攻击。Creature负责自己的属性、绘制、追踪 AI 和受击。ClawAttack负责攻击特效的绘制与生命周期。
这样当你需要新增一个“治疗道具”或者“爆炸技能”时,只需要新建一个类,然后在主循环中挂载对应的更新逻辑,而不需要改动猫和生物的代码。
6.2 属性随机要合理控制范围
“自创生物”听起来是纯随机的,但如果随机范围太夸张,比如生物速度从 0 到 10,或者半径从 5 到 100,就会导致部分生物完全追不上猫,或者画面被巨型生物遮挡。建议给每个属性设定一个合理的区间,并且可以在后续根据游戏进度动态调整:
# 示例:随击杀数提升生物难度 difficulty_scale = 1 + kill_count * 0.02 creature_speed = random.uniform(1.0, 2.5) * difficulty_scale6.3 碰撞检测的精度与性能取舍
圆形碰撞检测非常高效,但精度有限。如果生物形状非常不规则(比如触手怪),圆形检测会认为触手尖端也属于碰撞范围。对教学项目来说完全没问题。但如果你希望更精确,可以换成像素级碰撞检测:
pygame.sprite.collide_mask(cat, creature)使用collide_mask需要确保精灵的image带有透明通道,并且 Pygame 会自动根据非透明像素判断碰撞。这种方式开销更大,不建议同时检测太多精灵。
6.4 日志与调试信息
对于这种图形程序,直接print调试在窗口环境下不太方便。一个实用技巧是:把调试信息绘制到窗口上。比如在main.py中,把生物数量显示在左上角:
creature_count = len(creatures) debug_text = font.render(f"生物数量: {creature_count}", True, (255, 255, 255)) screen.blit(debug_text, (20, 90))这样你在调生物刷新频率和碰撞逻辑时,可以很直观地看到状态变化,比终端日志高效得多。
6.5 不要忽略“游戏结束”状态
很多初学者写完游戏主体后,就直接忽略了游戏结束状态的处理。但一个完整的游戏流程必须包含失败反馈和重新开始的机制。本文提供了一个简单的game_over布尔标记,以及重新开始时对精灵组的清理。你可以进一步扩展:
- 记录最高击杀数。
- 添加游戏倒计时。
- 死亡时播放简单音效。
6.6 性能优化小技巧
虽然这个项目规模很小,但养成性能意识很重要:
- 不要在
update中频繁创建新对象,比如每帧pygame.Surface都会造成较大的内存开销。本文中生物外观只在创建时绘制一次,后续只移动rect,这是合理做法。 pygame.sprite.Group内部会批量调用绘制的draw方法,比手动一个个blit更快。- 如果生物数量特别多(超过数百),可以按屏幕分块管理精灵,但一般教学项目不强制要求。
6.7 代码风格与注释
Pygame 项目很容易变成“一堆魔法数字”,例如50、25、9等。建议把关键数值定义为常量或类属性,例如:
class Cat: DEFAULT_SPEED = 4 ATTACK_COOLDOWN = 15 START_HP = 10这样后期调整数值时,不需要在代码中到处搜索“4”或“15”。注释也不需要每一行都写,但每个关键方法都应有简短的一句话用途说明。
7. 总结与学习路线
到这里,“卡通猫 vs 自创生物”这个小项目就已经完整跑通了。我们做了什么?
- 使用 Pygame 绘制了不依赖图片素材的卡通猫和自创生物。
- 用面向对象的方式把玩家、敌人、攻击特效封装成三个类。
- 实现了自创生物的“属性随机生成”机制,让每次点击都生成一个独一无二的敌人。
- 编写了生物追击 AI,让敌人朝玩家方向匀速移动。
- 实现了两种碰撞检测:生物碰到猫造成伤害,猫的爪击命中生物造成伤害。
- 加入了血量显示、击杀数统计、游戏结束与重新开始流程。
下一步,你可以沿着这些方向继续扩展:
- 给生物添加不同技能:比如会远程发射弹幕的生物、会分裂的生物、会加速冲锋的生物。
- 加入波次系统:每隔一定时间自动生成更多更强大的生物。
- 加入道具系统:拾取道具恢复血量,或者短时间无敌。
- 升级视觉效果:用粒子效果表现爪击,用发光效果表现生物被击中。
- 增加音效:Pygame 支持音频,可以给攻击和受击加上简单的声音反馈。
在学习过程中,建议你把代码中的每个数值都改一改,观察游戏行为的变化。比如把生物速度调成 5,把猫的攻击范围调大,把生物数量上限调低。只有自己亲手动过参数,才能真正理解这些数值如何影响游戏体验。
如果本文对你有帮助,欢迎收藏备用,也可以把它分享给同样在折腾 Pygame 的朋友。后续我会继续分享更多 Python 游戏开发和面向对象练手项目,有问题欢迎在评论区留言交流。