【python】开发了一个电子桌面桌宠,会要饭,满屏跑,效果太棒了
文档版本:1.0
适用平台:Windows
开发语言:Python 3.10
GUI 框架:PySide6
当前宠物:橘白猫「小橘」、藏獒「小山」
目录
- 项目概览
- 技术栈
- 项目结构
- 系统架构
- 模块详解
- 5.1 入口 & 应用生命周期 (main.py / app.py)
- 5.2 动画资源管理 (assets.py)
- 5.3 数据模型 (models.py)
- 5.4 动画状态机 (controller.py)
- 5.5 透明渲染窗口 (window.py)
- 5.6 设置持久化 (settings.py)
- 5.7 路径解析 (resources.py)
- 5.8 宠物选择 & 目录 (selection.py / pet_catalog.py)
- 动画清单格式 (animations.json)
- 交互系统
- 7.1 鼠标眼动追踪
- 7.2 拖拽与点击
- 7.3 自主行为
- 7.4 系统托盘
- 精灵生成管线
- 构建与打包
- 测试
- 配置与环境变量
1. 项目概览
本项目是一款离线的 Windows 桌面宠物应用。宠物以透明、无边框、始终置顶的窗口呈现在桌面上,支持自主行走、奔跑、休息,观察鼠标并响应用户的点击、拖拽、喂食等操作。
当前版本包含两只宠物:
| 宠物 | ID | 昵称 | 状态数 | 总帧数 | 步行动画 |
|---|---|---|---|---|---|
| 橘白猫 | orange_cat | 小橘 | 17 | 87 | 16 帧 |
| 藏獒 | tibetan_mastiff | 小山 | 17 | 137 | 20 帧 |
2. 技术栈
| 层级 | 技术 | 版本 | 用途 |
|---|---|---|---|
| 语言 | Python | ≥3.10 | 主体逻辑 |
| GUI | PySide6 | ≥6.6, <7 | 透明窗口、渲染、系统托盘 |
| 图像处理 | Pillow | ≥9.1, <12 | 精灵图加载与处理 |
| 打包 | PyInstaller | ≥5.1 | 生成独立 .exe |
| 精灵生成 | NumPy / OpenCV | ≥1.23 / ≥4.5 | 光流插值、色键去底 |
| 配置格式 | JSON | — | 动画清单、用户设置 |
| 设置存储 | Windows Registry | — | 开机自启注册表项 |
3. 项目结构
桌面宠物游戏/ ├── main.py # 应用入口 ├── run.bat # 快速启动脚本 ├── build.bat # 完整构建脚本 ├── pyproject.toml # 项目元数据、Ruff 配置 ├── requirements.txt # 运行时依赖 ├── requirements-dev.txt # 开发构建依赖 ├── orange_cat_pet.spec # PyInstaller 打包配置 │ ├── desktop_pet/ # 核心程序包 │ ├── __init__.py # 版本号 (1.0.0) │ ├── app.py # 应用生命周期协调器 │ ├── window.py # 透明宠物窗口 │ ├── controller.py # 动画状态机 │ ├── assets.py # 动画资源加载器 │ ├── models.py # 数据模型 & 枚举 │ ├── settings.py # 设置持久化 & 自启管理 │ ├── resources.py # 路径解析 (源码/打包) │ ├── selection.py # 宠物选择对话框 │ └── pet_catalog.py # 宠物定义目录 │ ├── assets/ # 游戏资源 │ ├── animations.json # 猫咪动画清单 │ ├── tibetan_mastiff_animations.json # 藏獒动画清单 │ ├── IMAGEGEN_PROMPTS.md # 精灵图 AI 生成提示词 │ ├── icons/orange_cat.ico # 应用图标 │ └── sprites/ # 精灵图表 / 生成帧 │ ├── generated/ # 猫咪已处理帧 (87 张 PNG) │ └── tibetan_mastiff/generated/ # 藏獒已处理帧 (137 张 PNG) │ ├── tools/ # 构建工具 │ ├── build_sprites.py # 猫精灵抽取 & 步态插值 │ ├── build_mastiff_sprites.py # 藏獒精灵抽取 & 动画生成 │ └── make_icon.py # Windows .ico 生成 │ ├── tests/ # 单元测试 │ ├── test_assets.py # 资源完整性测试 │ ├── test_models_settings.py # 设置序列化测试 │ ├── test_qt_smoke.py # Qt 烟雾测试 │ └── test_selection.py # 选择对话框测试 │ ├── build/orange_cat_pet/ # PyInstaller 构建中间产物 └── dist/OrangeCatPet/ # 最终发布目录4. 系统架构
┌──────────────────────────────────────────────────────────┐ │ main.py │ │ (入口, HIGHDPI 设置) │ └─────────────────┬────────────────────────────────────────┘ │ ┌─────────────────▼────────────────────────────────────────┐ │ DesktopPetApplication │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 应用协调: QApplication / SettingsStore │ │ │ │ 宠物切换: _activate_pet() / _choose_pet() │ │ │ │ 托盘管理: _create_or_refresh_tray() │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────┬────────────────────────────────────────┘ │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ ┌───────┐ ┌──────────┐ ┌─────────────┐ │Assets │ │Controller│ │Selection │ │Library│ │(状态机) │ │Dialog │ └───┬───┘ └────┬─────┘ └─────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────┐ │ PetWindow │ │ ┌───────────────────────────────┐ │ │ │ QWidget (透明, 置顶, 无边框) │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ QPainter 渲染当前帧 │ │ │ │ │ │ 眼球追踪计算 & 绘制 │ │ │ │ │ ├─────────────────────────┤ │ │ │ │ │ motion_timer (30ms) │ │ │ │ │ │ behaviour_timer (1s) │ │ │ │ │ │ single_click_timer │ │ │ │ │ ├─────────────────────────┤ │ │ │ │ │ 鼠标事件处理 │ │ │ │ │ │ 右键上下文菜单 │ │ │ │ │ └─────────────────────────┘ │ │ │ └───────────────────────────────┘ │ └─────────────────────────────────────┘5. 模块详解
5.1 入口 & 应用生命周期 (main.py / app.py)
main.py— 应用入口,设置QT_ENABLE_HIGHDPI_SCALING=1环境变量后创建并启动DesktopPetApplication。
DesktopPetApplication— 应用生命周期协调器 (desktop_pet/app.py:16),职责如下:
- 初始化
QApplication(应用名 “桌面宠物伙伴”,组织名 “OrangeCatDesktopPet”) setQuitOnLastWindowClosed(False)确保关闭窗口后隐藏到托盘而非退出run()方法:启动时先弹出宠物选择对话框,选择后进入 Qt 事件循环_activate_pet()方法:切换宠物时销毁旧窗口、重新加载动画库、创建新窗口、刷新托盘quit()方法:保存设置、隐藏托盘、退出应用
应用级信号流:
window.request_quit ──────────> app.quit() window.request_pet_selection ─> app.choose_pet()5.2 动画资源管理 (assets.py)
AnimationLibrary(desktop_pet/assets.py:12) — 从 JSON 动画清单文件加载并管理所有动画资源。
核心功能:
| 方法 | 说明 |
|---|---|
_load() | 解析 JSON 清单,为 17 个PetState构建AnimationClip |
clip(state) | 返回指定状态的AnimationClip |
pixmap(frame) | 惰性加载并缓存QPixmap(按路径缓存,避免重复文件 I/O) |
加载过程:
- 读取 JSON → 解析
canvas尺寸 - 遍历
PetState枚举 → 从animations对象中取出帧数组 - 每帧解析
path、duration_ms、eyes(眼球锚点)、eye_radius、hitbox - 校验文件存在性(缺失直接抛
FileNotFoundError) - 构建不可变
AnimationClip(frozendataclass)
5.3 数据模型 (models.py)
PetState(desktop_pet/models.py:9) — 17 种宠物状态的字符串枚举:
| 枚举值 | 中文 | 枚举值 | 中文 |
|---|---|---|---|
IDLE | 待机 | BLINK | 眨眼 |
WATCH | 观察 | WALK | 行走 |
RUN | 奔跑 | SIT | 坐下 |
LIE | 趴下 | SLEEP | 睡觉 |
WAKE | 醒来 | STRETCH | 伸懒腰 |
GROOM | 舔毛 | YAWN | 打哈欠 |
HAPPY | 开心 | SURPRISED | 惊讶 |
ANGRY | 生气 | EAT | 进食 |
DRAGGED | 被拖拽 |
FrameMetadata(desktop_pet/models.py:29) — 不可变帧数据:
| 字段 | 类型 | 说明 |
|---|---|---|
path | Path | 图片文件路径 |
duration_ms | int | 帧持续时间 (最小值 16ms) |
eyes | tuple[tuple[float, float], ...] | 眼球锚点坐标序列 |
eye_radius | tuple[float, float] | 瞳孔基准半径 (x, y) |
hitbox | tuple[int, int, int, int] | 碰撞检测区域 (x, y, w, h) |
AnimationClip(desktop_pet/models.py:38) — 不可变动画片段:
| 字段 | 类型 | 说明 |
|---|---|---|
state | PetState | 所属状态 |
frames | tuple[FrameMetadata, ...] | 帧序列 |
loop | bool | 是否循环播放 |
next_state | PetState | None | 非循环动画结束后的过渡状态 |
PetSettings(desktop_pet/models.py:46) — 用户设置数据类(可变),支持from_dict/to_dictJSON 序列化,含输入校验。
5.4 动画状态机 (controller.py)
PetController(desktop_pet/controller.py:9) — 管理动画状态切换与帧推进。
优先级系统:每个状态有优先级数值,高优先级可抢占低优先级(非循环动画播放中会锁住):
| 优先级 | 状态 |
|---|---|
| 100 | DRAGGED |
| 90 | EAT |
| 80 | HAPPY |
| 75 | SURPRISED,ANGRY |
| 65 | WAKE |
| 55 | STRETCH,GROOM,YAWN |
| 40 | SLEEP |
| 25 | RUN |
| 20 | WALK |
| 15 | WATCH |
| 10 | SIT,LIE |
| 8 | BLINK |
| 5 | IDLE |
状态切换逻辑 (set_state,controller.py:51):
if 新状态 == 当前状态 and not force → 忽略 if 当前非循环动画未播完 and not force and 新优先级 < 当前优先级 → 忽略 否则 → 切换状态, 重置帧索引, 发射信号, 重新调度定时器帧推进 (_advance,controller.py:76):
if 还有下一帧 → frame_index++ elif 循环动画 → 回到第 0 帧 else (非循环动画播完) → 过渡到 next_state (默认 IDLE) 发射 frame_changed → 重新调度定时器5.5 透明渲染窗口 (window.py)
PetWindow(desktop_pet/window.py:18) — 继承QWidget,所有渲染与交互的核心。
窗口属性
| 属性 | 值 |
|---|---|
| 固定尺寸 | library.canvas_size(256×256) |
WA_TranslucentBackground | True |
WA_NoSystemBackground | True |
autoFillBackground | False |
| 窗口标志 | FramelessWindowHint | Tool | WindowStaysOnTopHint |
定时器
| 定时器 | 间隔 | 用途 |
|---|---|---|
motion_timer | 30ms | 行走/奔跑位移 ±2px (走) / ±5px (跑) |
behaviour_timer | 1s | 饥饿/心情更新、随机行为决策 |
single_click_timer | 单次 | 区分单击与双击 |
渲染管线 (paintEvent)
1. QPainter(painter) 描画到 Widget 2. 判断朝向: facing_right 决定是否水平翻转 3. 绘制精灵: drawPixmap(target_rect, pixmap) 4. 眼球追踪计算: a. 获取全局鼠标位置 QCursor.pos() b. 映射到 Widget 局部坐标 c. 遍历 frame.eyes 中每只眼睛的锚点 d. 计算方向向量 → 归一化 → 瞳孔偏移量 e. 绘制白色虹膜 + 黑色瞳孔 + 白色高光点多显示器支持
- 使用
QApplication.screenAt(center)获取当前所在屏幕 - 使用
availableGeometry()获取不含任务栏的工作区 - 移动时通过
_clamped_position()约束宠物不出工作区边界
右键菜单
动态构建QMenu,包含以下选项:
| 选项 | 功能 |
|---|---|
| 喂食 | 切换到EAT状态 |
| 召回 | 将宠物移到当前屏幕中心底部 |
| 选择宠物 | 弹出选择对话框 |
| 暂停 | 冻结/恢复动画和行为 |
| 置顶 | 切换WindowStaysOnTopHint |
| 开机启动 | 写入/删除注册表自启项 |
| 退出 | 保存设置 → 完全退出 |
5.6 设置持久化 (settings.py)
SettingsStore(desktop_pet/settings.py:22) — JSON 设置文件的读写封装。
| 存储路径 | 说明 |
|---|---|
%LOCALAPPDATA%\OrangeCatDesktopPet\settings.json | 默认路径 |
由ORANGE_CAT_DATA_DIR环境变量覆盖 | 测试用途 |
原子保存机制 (save,settings.py:35):
写入 .tmp 文件 → 调用 Path.replace() 原子替换原文件容错机制 (load,settings.py:26):
任何异常 (OSError, ValueError, TypeError, JSONDecodeError) → 返回默认 PetSettingsAutoStartManager(desktop_pet/settings.py:45) — 通过操作HKCU\Software\Microsoft\Windows\CurrentVersion\Run注册表键实现开机自启。
is_enabled()— 读取注册表判断是否已有启动项set_enabled(bool)— 写入或删除注册表值command()— 生成正确的启动命令行(区分源码运行 vs PyInstaller 打包)
5.7 路径解析 (resources.py)
resource_path(relative_path)— 统一路径解析:
if sys.frozen (PyInstaller 打包): 返回 Path(sys._MEIPASS) / relative_path else: 返回 Path(__file__).resolve().parents[1] / relative_path支持参数形式:
resource_path("assets/animations.json")— 字符串resource_path(Path("assets/sprites/generated/idle/00.png"))—Path对象
5.8 宠物选择对话框 (selection.py / pet_catalog.py)
PetSelectionDialog(desktop_pet/selection.py) — 模态对话框 (960×630),首次启动时弹出,其后可通过右键菜单打开。
- 展示所有宠物的预览卡片(图片、名称、描述、选中按钮)
- 当前已选宠物高亮显示
- 点击确定后触发
app._activate_pet()
PetDefinition(desktop_pet/pet_catalog.py) — 宠物定义的不可变 dataclass:
| 字段 | 类型 | 说明 |
|---|---|---|
pet_id | str | 宠物唯一标识 |
display_name | str | 中文显示名 |
description | str | 简短描述 |
manifest | Path | 动画清单路径 |
preview_image | Path | 选择页预览图路径 |
frame_count | int | 总帧数 |
当前宠物目录:
- 小橘:
orange_cat→assets/animations.json(87 帧) - 小山:
tibetan_mastiff→assets/tibetan_mastiff_animations.json(137 帧)
6. 动画清单格式 (animations.json)
{ "version": 2, "canvas": [256, 256], "animations": { "idle": { "frames": [ { "path": "assets/sprites/generated/idle/00.png", "duration_ms": 100, "eyes": [[120.3, 80.5], [140.2, 80.5]], "eye_radius": [5.0, 4.0], "hitbox": [10, 20, 236, 236] } ], "loop": true }, "eat": { "frames": [ /* ... */ ], "loop": false, "next_state": "idle" } } }字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
version | int | 否 | 清单格式版本 |
canvas | [int, int] | 是 | 画布尺寸 |
animations.<state>.frames | array | 是 | 帧数组 (至少 1 帧) |
frames[].path | string | 是 | 相对路径 |
frames[].duration_ms | int | 是 | 帧显示时长 (ms) |
frames[].eyes | [[float, float]] | 否 | 眼球锚点坐标 |
frames[].eye_radius | [float, float] | 否 | 瞳孔半径,默认 [5, 4] |
frames[].hitbox | [int, int, int, int] | 否 | 点击碰撞区 |
animations.<state>.loop | bool | 否 | 是否循环,默认true |
animations.<state>.next_state | string | 否 | 播完后过渡到的状态 |
7. 交互系统
7.1 鼠标眼动追踪
每帧渲染时实时计算瞳孔位置,产生「宠物注视鼠标」的效果。
算法步骤:
1. 获取全局鼠标坐标: QCursor.pos() 2. 映射到 Widget 坐标系: widget->mapFromGlobal(global_pos) 3. 计算宠物中心: (width/2, height/2) 4. 对每只眼睛的锚点 (eye_x, eye_y): 5. dx = mouse_x - eye_x 6. dy = mouse_y - eye_y 7. dist = sqrt(dx² + dy²) 8. scale = 1 - clamp(dist / max_distance, 0, 1) 9. pupil_x = eye_x + normalize(dx) * max_offset * scale 10. pupil_y = eye_y + normalize(dy) * max_offset * scale 11. 绘制: - QColor(255, 255, 255, 220) 画白色虹膜 - QColor(20, 20, 20, 235) 画黑色瞳孔在偏移位置 - QColor(255, 255, 255, 180) 画小白色高光点7.2 拖拽与点击
| 事件 | 处理方式 |
|---|---|
mousePressEvent | 记录拖拽起点;启动单击计时器 (300ms) |
mouseMoveEvent | 超过拖拽阈值 (4px) 后进入DRAGGED状态;实时更新窗口位置 |
mouseReleaseEvent | 结束拖拽,回到IDLE;保存位置 |
mouseDoubleClickEvent | 取消单击计时器;切换到HAPPY状态 |
| 单击超时 (300ms) | 切换到SURPRISED状态 |
contextMenuEvent | 弹出右键菜单 |
7.3 自主行为
behaviour_timer(1 秒间隔) 执行以下逻辑:
- 饥饿值更新:每秒 -0.02,高活跃度状态额外 -0.03
- 心情值更新:非暂停状态下微调
- 随机行为决策:根据饥饿值、心情值、当前状态,概率性切换到行走、奔跑、坐下、趴下、睡觉、舔毛、伸懒腰、打哈欠、眨眼等状态
- 边缘弹跳:碰到屏幕边缘时调转方向 (
facing_right = not facing_right)
7.4 系统托盘
| 操作 | 效果 |
|---|---|
| 单击 / 双击托盘图标 | 召回宠物 (call_home()) |
| 托盘图标 | 使用宠物HAPPY状态第一帧作为图标 |
| 托盘 Tooltip | 显示{宠物名}桌宠 |
| 托盘右键菜单 | 与窗口右键菜单相同 |
8. 精灵生成管线
精灵制作与处理全流程由tools/build_sprites.py和tools/build_mastiff_sprites.py实现。
整体流程
原始 4×4 姿态图集 (cat_pose_atlas.png / mastiff_pose_atlas_v2.png) │ ▼ ┌─────────────────────────────────────────────┐ │ 1. 提取: 4×4 网格分割,重叠区域扩展 │ │ 最大连通分量提取 → 透明背景角色 │ │ 2. 归一化: 各姿态统一到 256×256 画布 │ │ 保持底部基线对齐 │ │ 3. 去底: 洋红色 (#ff00ff) 色键 → 透明通道 │ │ 溢出色彩去除 (spill removal) │ └─────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ 步态关键帧 (walk_cycle_v5.png 的 4 张 │ │ 极值姿势: 左前/左后/右前/右后) │ │ │ │ │ ▼ │ │ Farneback 光流插值 (OpenCV) │ │ - 猫: 4 关键帧 → 3 中间帧/段 → 16 帧 │ │ - 藏獒: 4 关键帧 → 4 中间帧/段 → 20 帧 │ │ - RGBA 预乘处理避免透明边缘伪影 │ │ │ │ │ ▼ │ │ 状态动画: 对各姿态施加微妙运动变化 │ │ (位移 / 缩放 / 旋转) 实现呼吸、弹跳等效果 │ └─────────────────────────────────────────────┘ │ ▼ 输出: generated/ + animations.json │ ▼ make_icon.py → orange_cat.ico光流插值关键细节
- 使用 OpenCV
calcOpticalFlowFarneback对预乘 alpha 的 RGBA 数据做稠密光流估计 - 每个像素通道独立插值,alpha 通道参与计算但插值后 clamp 到 [0,255]
- 透明背景区域(alpha == 0)的 RGB 在插值前清零,避免「透明像素 RGB 污染」
9. 构建与打包
环境准备
python-m pip install-r requirements.txt# 运行时依赖python-m pip install-r requirements-dev.txt# 构建依赖完整构建流程
build.bat按顺序执行:
tools/build_sprites.py → 生成猫咪精灵 tools/make_icon.py → 生成应用图标 pyinstaller --noconfirm --clean orange_cat_pet.spec → 打包PyInstaller 配置 (orange_cat_pet.spec)
关键配置项:
- 入口脚本:
main.py - 窗口模式:
console=False(不显示控制台窗口) - 包含资源目录:
assets/整体打包进_MEIPASS - 额外二进制 / 数据文件:通过
TOC清单指定
输出
dist/OrangeCatPet/OrangeCatPet.exe ← 最终可执行文件10. 测试
运行测试
$env:QT_QPA_PLATFORM="offscreen"$env:ORANGE_CAT_DATA_DIR="$PWD\.runtime\test-data"python-m unittest discover-s tests-v测试覆盖
| 测试文件 | 覆盖范围 |
|---|---|
test_assets.py | 动画清单完整性、RGBA 图片验证、尺寸一致性、步态帧数、色键去底效果 |
test_models_settings.py | PetSettings序列化/反序列化、边界值校验、默认值恢复 |
test_qt_smoke.py | Qt 环境可用性、窗口创建、动画剪辑加载 |
test_selection.py | 选择对话框 UI 元素、宠物卡片渲染 |
11. 配置与环境变量
运行环境
| 环境变量 | 说明 | 默认值 |
|---|---|---|
QT_ENABLE_HIGHDPI_SCALING | 启用高 DPI 缩放 | 1 |
ORANGE_CAT_DATA_DIR | 设置文件存储目录 (用于测试) | 无 |
QT_QPA_PLATFORM | Qt 平台插件 (测试用) | 无 |
设置文件
- 路径:
%LOCALAPPDATA%\OrangeCatDesktopPet\settings.json - 格式: JSON
- 字段:
selected_pet,x,y,volume,always_on_top,autostart,hunger,mood,paused
开机自启
Windows 注册表项:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run └── OrangeCatDesktopPet = "<pythonw.exe路径>" "<main.py路径>"源码运行时使用pythonw.exe(无控制台启动),打包后直接指向OrangeCatPet.exe。
本文档描述的项目版本为 1.0.0,对应
pyproject.toml中定义的版本。
若想要获取代码和游戏 ,绿泡泡搜索 “码来的小朋友” 然后发送回复“14桌面宠物” 即可获取。
