Python游戏开发实战:如何用不到100行代码实现一个简易FPS游戏
Python游戏开发实战:如何用不到100行代码实现一个简易FPS游戏
我一直觉得,游戏开发的门槛被过分夸大了。很多人一听到“第一人称射击游戏”,脑海里立刻浮现出庞大的引擎、复杂的图形管线、动辄几十万行的代码。但技术的魅力恰恰在于,我们可以用最简洁的工具,去触及那些看似高不可攀的核心。今天,我们就来打破这个迷思,用Python,在不到100行代码的篇幅里,亲手搭建一个能跑、能玩、有射击反馈的简易FPS游戏原型。这不仅仅是一个编码练习,更是一次对游戏本质的探索——剥离华丽的画面,剩下的核心玩法循环究竟是什么?
这个项目非常适合那些已经掌握了Python基础语法,对pygame库有初步了解,并且渴望快速看到成果的开发者。我们不会涉及复杂的3D数学或高级渲染技术,而是聚焦于用最直观的二维精灵(Sprite)模拟出第一人称的视角和交互感。你将亲手实现玩家移动、视角控制、射击判定和目标交互这几个核心模块。整个过程就像搭积木,每一块代码都有明确的职责,组合起来便是一个完整的、可交互的游戏世界。让我们暂时忘掉那些需要数月学习的重型引擎,用一下午的时间,感受从零到一创造游戏的纯粹乐趣。
1. 环境准备与核心思路
在开始敲代码之前,我们需要明确两件事:用什么工具,以及要实现什么效果。我们的目标是“简易FPS”,这意味着我们将做出一些巧妙的简化。真正的3D FPS需要处理三维坐标系、透视投影、光照和纹理,这远超100行代码的范畴。因此,我们的策略是:用二维的精灵和画布,模拟出第一人称的视觉体验。
具体来说,我们会把游戏窗口视为玩家的“视野”。所有的敌人、物体都将作为精灵存在于一个二维平面上,但它们的位置和大小会根据一个虚拟的“玩家位置”和“视角方向”进行计算和渲染,从而产生距离感和空间感。当玩家移动或转动视角时,我们重新计算所有精灵在屏幕上的投影位置,营造出第一人称移动的错觉。
提示:这种简化方法在早期游戏和许多创意游戏中非常常见,它牺牲了绝对的物理真实感,换来了极高的开发效率和运行性能,非常适合原型设计和概念验证。
首先,确保你的开发环境已经就绪。你需要安装Python(建议3.7及以上版本)以及我们将要使用的核心库pygame。
pip install pygame
接下来,创建一个新的Python文件,比如命名为 simple_fps.py。我们将在这个文件中构建整个游戏。先导入必要的模块,并初始化Pygame。
import pygame
import sys
import math
import random
# 初始化pygame
pygame.init()
2. 构建游戏世界与玩家视角
游戏的核心是世界的表示和玩家与世界的交互。我们定义一个简单的“游戏世界”,它包含一个玩家和若干个敌人。玩家的状态由其在世界中的位置(x, y)和面向的角度(angle)决定。
# 游戏窗口设置
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("简易FPS - 百行代码挑战")
clock = pygame.time.Clock()
# 玩家属性
class Player:
def __init__(self, x, y):
self.x = x # 世界坐标X
self.y = y # 世界坐标Y
self.angle = 0 # 面向角度(弧度)
self.height = 20 # 玩家视线高度(用于模拟)
self.move_speed = 0.05
self.rotate_speed = 0.03
player = Player(0, 0)
# 敌人属性:用列表存储,每个敌人是一个字典,包含位置和状态
enemies = [
{"x": 5, "y": 5, "alive": True, "size": 30},
{"x": -3, "y": 8, "alive": True, "size": 25},
{"x": 7, "y": -2, "alive": True, "size": 35},
]
现在,我们有了一个位于原点(0,0)的玩家,和三个散落在世界中的敌人。但如何将这个世界以第一人称视角渲染到屏幕上呢?这里需要一个关键的投影函数。它的作用是将一个世界坐标(x, y)的点,根据玩家的位置和视角,计算其应该在屏幕上的绘制位置和大小,从而产生近大远小的透视效果。
def project_to_screen(world_x, world_y, obj_size):
"""将世界坐标投影到屏幕"""
# 计算物体相对于玩家的坐标
dx = world_x - player.x
dy = world_y - player.y
# 计算物体在玩家视角下的距离和角度
distance = math.sqrt(dx*dx + dy*dy)
# 防止除零错误,并设置一个最小距离
if distance < 0.1:
distance = 0.1
# 计算物体与玩家朝向的夹角
obj_angle = math.atan2(dy, dx)
relative_angle = obj_angle - player.angle
# 将相对角度归一化到[-π, π]区间
while relative_angle > math.pi:
relative_angle -= 2 * math.pi
while relative_angle < -math.pi:
relative_angle += 2 * math.pi
# 简单的透视投影:物体在屏幕上的大小与距离成反比
screen_size = obj_size * 100 / distance
# 物体在屏幕上的水平位置与其相对角度有关(视角越偏,屏幕位置越偏)
screen_x = SCREEN_WIDTH // 2 + (relative_angle * 300)
# 物体在屏幕上的垂直位置与距离有关(模拟地平线)
screen_y = SCREEN_HEIGHT // 2 - (100 / distance)
# 返回屏幕坐标和绘制大小,以及距离(用于后续判断)
return int(screen_x), int(screen_y), int(screen_size), distance
这个函数是模拟3D感的核心魔法。它通过数学计算,将二维世界坐标映射到二维屏幕坐标,并让远处的物体看起来更小、更高(接近地平线)。relative_angle决定了物体在屏幕上是偏左还是偏右。
3. 实现玩家控制与游戏主循环
一个游戏如果没有交互,就只是一段动画。接下来,我们要处理玩家的输入,让世界“活”起来。我们将通过键盘(WASD)控制移动,通过鼠标控制视角旋转。同时,我们还需要一个持续运行的游戏主循环,来处理事件、更新状态和重绘画面。
# 初始化鼠标并隐藏光标(为了第一人称体验)
pygame.mouse.set_visible(False)
pygame.event.set_grab(True) # 将鼠标锁定在窗口内
# 游戏主循环
running = True
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
# --- 处理键盘输入(移动)---
keys = pygame.key.get_pressed()
move_x, move_y = 0, 0
if keys[pygame.K_w]: # 前进
move_x += math.cos(player.angle) * player.move_speed
move_y += math.sin(player.angle) * player.move_speed
if keys[pygame.K_s]: # 后退
move_x -= math.cos(player.angle) * player.move_speed
move_y -= math.sin(player.angle) * player.move_speed
if keys[pygame.K_a]: # 左平移
move_x += math.cos(player.angle - math.pi/2) * player.move_speed
move_y += math.sin(player.angle - math.pi/2) * player.move_speed
if keys[pygame.K_d]: # 右平移
move_x += math.cos(player.angle + math.pi/2) * player.move_speed
move_y += math.sin(player.angle + math.pi/2) * player.move_speed
player.x += move_x
player.y += move_y
# --- 处理鼠标输入(视角旋转)---
mouse_rel = pygame.mouse.get_rel() # 获取鼠标自上一帧以来的相对移动量
player.angle += mouse_rel[0] * player.rotate_speed
# 清屏,绘制天空(上部分)和地面(下部分)
screen.fill((135, 206, 235)) # 天蓝色天空
pygame.draw.rect(screen, (101, 67, 33), (0, SCREEN_HEIGHT//2, SCREEN_WIDTH, SCREEN_HEIGHT//2)) # 棕色地面
在上面的循环中,我们首先处理退出事件。然后,通过pygame.key.get_pressed()获取持续按下的键位状态,并根据玩家当前角度计算出前进、后退、平移的位移向量。视角旋转则通过pygame.mouse.get_rel()获取鼠标的横向移动量来实现,这是第一人称游戏中非常经典的控制方式。最后,我们用两种颜色填充屏幕,简单模拟出天空和地面,为后续绘制的物体提供一个背景。
4. 渲染敌人与实现射击机制
有了可移动的视角和投影函数,现在可以把敌人画出来了。我们遍历enemies列表,将每个存活的敌人通过project_to_screen函数计算其屏幕位置和大小,然后用一个简单的圆形(或矩形)绘制出来。同时,我们还需要实现射击功能:当玩家点击鼠标左键时,从屏幕中心(代表枪的准星)发射一条“射线”,检测是否击中了敌人。
首先,在游戏主循环的输入处理部分,加入鼠标按键检测:
# 在主循环的事件处理部分之后,更新状态之前,检测鼠标按键
mouse_buttons = pygame.mouse.get_pressed()
if mouse_buttons[0]: # 鼠标左键被按下
# 射击逻辑将在这里实现
pass
然后,在清屏和绘制背景之后,添加渲染敌人和射击判定的代码:
# --- 渲染所有敌人 ---
for enemy in enemies:
if enemy["alive"]:
screen_x, screen_y, screen_size, distance = project_to_screen(enemy["x"], enemy["y"], enemy["size"])
# 简单裁剪:只绘制在屏幕范围内的敌人
if 0 < screen_x < SCREEN_WIDTH and 0 < screen_y < SCREEN_HEIGHT:
# 根据距离调整颜色亮度(越远越暗)
brightness = max(50, 255 - int(distance * 20))
color = (brightness, 50, 50) # 红色系敌人
pygame.draw.circle(screen, color, (screen_x, screen_y), screen_size // 2)
# 画一个“眼睛”或标识
pygame.draw.circle(screen, (255, 255, 255), (screen_x - screen_size//6, screen_y - screen_size//6), screen_size//8)
# --- 射击判定(在按下鼠标左键时执行)---
if mouse_buttons[0]:
# 我们假设准星在屏幕正中心 (SCREEN_WIDTH//2, SCREEN_HEIGHT//2)
crosshair_x, crosshair_y = SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2
hit_enemy_index = None
closest_distance = 50 # 设定一个最大有效射击距离
# 遍历所有敌人,判断是否被“击中”
for i, enemy in enumerate(enemies):
if not enemy["alive"]:
continue
# 计算敌人在屏幕上的投影
screen_x, screen_y, screen_size, distance = project_to_screen(enemy["x"], enemy["y"], enemy["size"])
# 计算准星到敌人投影中心的距离
dx = crosshair_x - screen_x
dy = crosshair_y - screen_y
dist_to_crosshair = math.sqrt(dx*dx + dy*dy)
# 如果距离小于敌人的显示半径,且敌人在有效射程内,则认为击中
if dist_to_crosshair < screen_size // 2 and distance < closest_distance:
closest_distance = distance
hit_enemy_index = i
# 如果找到了被击中的敌人,更新其状态
if hit_enemy_index is not None:
enemies[hit_enemy_index]["alive"] = False
# 可以在这里添加击中音效或特效
print(f"击中了敌人 {hit_enemy_index}!")
# --- 绘制准星 ---
crosshair_color = (255, 255, 255)
crosshair_size = 10
pygame.draw.line(screen, crosshair_color, (SCREEN_WIDTH//2 - crosshair_size, SCREEN_HEIGHT//2),
(SCREEN_WIDTH//2 + crosshair_size, SCREEN_HEIGHT//2), 2)
pygame.draw.line(screen, crosshair_color, (SCREEN_WIDTH//2, SCREEN_HEIGHT//2 - crosshair_size),
(SCREEN_WIDTH//2, SCREEN_HEIGHT//2 + crosshair_size), 2)
# 更新屏幕显示
pygame.display.flip()
# 控制帧率
clock.tick(60)
# 退出游戏
pygame.quit()
sys.exit()
至此,一个功能完整的简易FPS游戏原型就完成了。你可以使用WASD键在场景中移动,移动鼠标来环顾四周,并用鼠标左键射击屏幕上出现的红色“敌人”。当准星对准敌人并点击时,敌人会消失,并在控制台输出击中信息。
5. 优化、扩展与思考
虽然核心功能已经实现,但目前的代码在可玩性和健壮性上还有很大提升空间。我们可以从以下几个方向进行优化和扩展,这不仅能提升游戏体验,也是深入理解游戏开发逻辑的好机会。
1. 增加视觉反馈与音效 当前的射击反馈只有控制台打印,这显然不够。我们可以很容易地添加音效和粒子特效。
- 音效:在射击命中时,播放一个简短的“击中”音效。你需要准备一个
.wav或.ogg格式的音效文件。# 在初始化部分加载音效 try: hit_sound = pygame.mixer.Sound('hit.wav') except: print("未找到音效文件,将继续无声运行。") hit_sound = None # 在击中敌人的代码块中播放音效 if hit_enemy_index is not None: enemies[hit_enemy_index]["alive"] = False if hit_sound: hit_sound.play() - 特效:击中敌人时,可以在敌人位置短暂绘制一个爆炸或血花效果。这可以通过在击中位置添加一个短暂存在的、会逐渐缩小或消失的精灵来实现。
2. 改进敌人AI与游戏逻辑 让敌人静止不动显然太简单了。我们可以为敌人添加简单的行为模式。
- 巡逻与追击:为每个敌人添加一个状态(如“闲置”、“巡逻”、“追击”)。当玩家进入一定范围,敌人状态转为“追击”并向玩家移动。
- 攻击与生命值:为玩家添加生命值,当敌人足够接近时,玩家会受到伤害。同时,敌人也可能需要多次射击才能被击败。
3. 性能优化与代码结构 随着游戏元素增多,代码需要更好的组织。
- 使用精灵组:将玩家和敌人都定义为继承自
pygame.sprite.Sprite的类,并使用pygame.sprite.Group进行管理,可以简化碰撞检测和绘制逻辑。 - 状态管理:将游戏的不同阶段(如开始菜单、游戏中、游戏结束)用明确的游戏状态机来管理,使逻辑更清晰。
4. 从“玩具”到“工具”的思维转变 完成这个项目后,你不应只满足于这100行代码。更重要的是理解其背后的范式:用简单的规则和抽象模拟复杂的体验。这个简易FPS的核心是一个基于角度和距离的投影系统,它本质上是一个极简的软件渲染器。许多独立游戏和创意原型都采用了类似的思路。
你可以尝试修改投影函数中的参数(如300、100等),观察它们如何影响视角的广度和透视的强度。你甚至可以尝试引入高度(Z轴)的概念,让物体不仅能水平移动,还能有上下位置,从而模拟出楼梯、跳跃等更丰富的场景。这个小小的代码框架,是一个绝佳的实验沙盒。
当我第一次运行这个自己写的简易FPS时,那种操控感带来的兴奋是难以言喻的。尽管画面简陋,但移动、转身、瞄准、射击这一系列动作的闭环被成功打通,这证明了核心玩法的可行性。在后续的迭代中,我尝试加入了随机生成的地图(用二维数组表示障碍物),并修改投影函数让墙壁也能被渲染出来,整个过程就像在亲手搭建一个游戏引擎的雏形。这种从底层理解交互和渲染逻辑的经历,远比直接使用一个成熟引擎更能锻炼解决问题的能力。如果你对某个部分感到好奇,比如“如何让子弹有飞行时间”或“如何实现不同种类的武器”,最好的方式就是基于现在这个框架,大胆地修改和实验。代码就在你手中,世界由你定义。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)