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的核心是一个基于角度和距离的投影系统,它本质上是一个极简的软件渲染器。许多独立游戏和创意原型都采用了类似的思路。

你可以尝试修改投影函数中的参数(如300100等),观察它们如何影响视角的广度和透视的强度。你甚至可以尝试引入高度(Z轴)的概念,让物体不仅能水平移动,还能有上下位置,从而模拟出楼梯、跳跃等更丰富的场景。这个小小的代码框架,是一个绝佳的实验沙盒。

当我第一次运行这个自己写的简易FPS时,那种操控感带来的兴奋是难以言喻的。尽管画面简陋,但移动、转身、瞄准、射击这一系列动作的闭环被成功打通,这证明了核心玩法的可行性。在后续的迭代中,我尝试加入了随机生成的地图(用二维数组表示障碍物),并修改投影函数让墙壁也能被渲染出来,整个过程就像在亲手搭建一个游戏引擎的雏形。这种从底层理解交互和渲染逻辑的经历,远比直接使用一个成熟引擎更能锻炼解决问题的能力。如果你对某个部分感到好奇,比如“如何让子弹有飞行时间”或“如何实现不同种类的武器”,最好的方式就是基于现在这个框架,大胆地修改和实验。代码就在你手中,世界由你定义。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐