ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用Python和Pygame打造会撒娇的智能桌面宠物:从状态机到情感交互

2026/8/10 10:42:04 拓冰建站 浏览量
用Python和Pygame打造会撒娇的智能桌面宠物:从状态机到情感交互

1. 项目概述:从像素动画到情感交互的桌面伙伴

还记得十几年前电脑右下角那只只会傻傻走动的电子宠物吗?那时候的桌面宠物,更多是一种简单的像素动画,功能单一,互动性几乎为零。如今,随着Python生态的成熟和图形库的易用性,我们完全有能力赋予桌面宠物新的生命——让它成为一个能感知、会互动、甚至懂得“撒娇”的智能伙伴。这个项目,就是用Python的pygame库,亲手打造一个具备基础情感交互能力的桌面宠物。它不仅仅是一个停留在屏幕上的动画精灵,而是一个能响应你的鼠标动作、根据你的互动频率改变情绪状态,并做出相应反馈的“活物”。

这个项目非常适合有一定Python基础,想从命令行世界迈向图形界面和交互逻辑开发的开发者。它不涉及复杂的算法,但涵盖了事件处理、状态机、精灵动画、资源管理等游戏开发的核心概念。通过完成它,你不仅能收获一个独一无二的桌面伙伴,更能透彻理解一个交互式应用从设计到实现的完整流程。下面,我将带你从零开始,拆解每一个技术环节,并附上完整的、可直接运行的源码。

2. 核心设计思路:状态驱动的情感模型

一个会“撒娇”的宠物,其核心在于它需要具备内在的“情感”或“状态”,并且这些状态能够通过可视化的行为表现出来。我们不能让它随机地播放动画,那样就太“傻”了。因此,整个项目的设计核心是一个基于状态机的宠物行为模型

2.1 情感状态的定义与转换

首先,我们需要为宠物定义几种基础情感状态。这类似于游戏角色的状态机,状态决定了当前可以执行的行为。一个简单而有效的模型可以包含以下几种状态:

  1. 空闲 (IDLE):宠物的默认状态。此时它可能在进行一些周期性的小动作,比如眨眼、轻微晃动。
  2. 高兴 (HAPPY):当用户与它积极互动(如频繁抚摸、点击)时触发。表现为播放欢快的动画,发出愉悦的音效。
  3. 撒娇 (ACTING CUTE):这是本项目的特色状态。当宠物处于高兴状态一段时间,或者接收到特定的互动指令(如双击)时,会进入此状态。表现为做出特别可爱的动作,比如打滚、举起爪子、冒出爱心气泡等。
  4. 困倦 (SLEEPY):当一段时间没有用户互动时触发。宠物动作变慢,开始打哈欠,眼皮打架。
  5. 睡觉 (SLEEPING):由困倦状态自然过渡或用户长时间不互动后进入。播放睡觉的呼吸动画,对轻微互动无反应。

这些状态之间的转换不是随机的,而是由一系列规则驱动,我称之为“情感能量”系统。我们可以为宠物设定一个内部的“心情值”或“能量值”。例如:

  • 用户每次点击(抚摸)宠物,心情值 +10。
  • 每秒自动衰减 -1。
  • 当心情值 > 50 时,进入HAPPY状态。
  • HAPPY状态下持续5秒且心情值维持高位,则有机会(例如30%概率)进入ACTING CUTE状态。
  • 当心情值 < 10 且持续10秒,进入SLEEPY状态。
  • SLEEPY状态下持续5秒,进入SLEEPING状态。
  • SLEEPING状态时被用力摇晃(模拟快速连续点击),心情值大幅增加并唤醒至IDLE

这套逻辑是宠物显得“智能”和“有生命感”的关键。所有动画和音效的播放,都严格依赖于当前的状态。

2.2 精灵动画与资源管理

确定了行为逻辑,接下来就是如何呈现。Pygame 提供了pygame.sprite.Sprite类来高效管理游戏中的对象。我们的宠物将继承这个类。

动画实现:对于2D精灵,最常见的动画方式是“精灵图”(Sprite Sheet)或序列帧动画。我们将为宠物的每一个状态准备一组连续的图片。例如,“撒娇”状态可能包含8张从趴下到打滚的图片。在代码中,我们需要一个逻辑来根据当前状态和状态内的时间,计算当前应该显示哪一帧图片。

# 伪代码示例:动画更新逻辑 def update_animation(self, dt): # dt 为距离上一帧的时间 self.frame_timer += dt if self.frame_timer >= self.frame_duration: self.frame_timer = 0 self.current_frame_index = (self.current_frame_index + 1) % len(self.frames[self.state]) self.image = self.frames[self.state][self.current_frame_index] # 更新显示的图片

资源管理:一个项目会有大量图片、音效文件。好的做法是创建一个资源加载模块,使用字典来归类存储,避免全局散落的变量。例如:

resources = { ‘images’: { ‘pet_idle’: [img1, img2, ...], ‘pet_happy’: [img1, img2, ...], ‘pet_acting_cute’: [img1, img2, ...], # 撒娇专属动画帧 ‘heart_bubble’: heart_img, # 撒娇时冒出的爱心 }, ‘sounds’: { ‘meow_happy’: pygame.mixer.Sound(‘happy.wav’), ‘purr’: pygame.mixer.Sound(‘purr.wav’), } }

注意:图片素材的风格统一性至关重要。如果你自己绘制,请确保所有状态下的精灵大小、比例、色调一致。如果从网络获取,也要注意授权问题。音效最好选择无刺耳感、循环播放也不恼人的短促声音。

3. 关键技术实现与代码拆解

有了设计蓝图,我们开始动手搭建。这里会深入到几个核心模块的代码实现,并解释每一步的意图。

3.1 工程初始化与主循环架构

首先,我们搭建Pygame程序的标准骨架。主循环是游戏的心脏,它以固定的频率(帧率)不断处理事件、更新逻辑、重绘画面。

import pygame import sys import os from enum import Enum # 初始化pygame pygame.init() pygame.mixer.init() # 初始化音效 # 屏幕设置 SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.NOFRAME) # NOFRAME 实现无边框窗口 pygame.display.set_caption(“我的智能桌面宠物”) clock = pygame.time.Clock() FPS = 60 # 设定帧率,控制动画流畅度 # 定义宠物状态枚举,比直接用字符串更规范 class PetState(Enum): IDLE = “idle” HAPPY = “happy” ACTING_CUTE = “acting_cute” SLEEPY = “sleepy” SLEEPING = “sleeping” class DesktopPet(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() # 状态与属性初始化 self.state = PetState.IDLE self.mood_value = 50 # 初始心情值 self.state_timer = 0 # 记录处于当前状态的时间 self.frames = {} # 存储各状态动画帧 self.load_resources() self.rect = self.image.get_rect(center=(x, y)) self.dragging = False # 是否被拖拽 def load_resources(self): # 这里应实现从文件加载图片和音效到 self.frames 和 self.sounds # 示例:加载空闲状态的两帧动画 self.frames[PetState.IDLE] = [ pygame.image.load(‘pet_idle_0.png’).convert_alpha(), pygame.image.load(‘pet_idle_1.png’).convert_alpha(), ] self.current_frame_index = 0 self.image = self.frames[self.state][self.current_frame_index] self.animation_timer = 0 self.frame_duration = 0.2 # 每帧显示0.2秒 def update(self, dt, events): # 处理事件(如点击) self.handle_events(events) # 更新内部状态逻辑(心情衰减、状态转换判断) self.update_state_logic(dt) # 更新动画 self.update_animation(dt) # 更新位置(如果被拖拽) if self.dragging: self.rect.center = pygame.mouse.get_pos() def handle_events(self, events): for event in events: if event.type == pygame.MOUSEBUTTONDOWN: if self.rect.collidepoint(event.pos): if event.button == 1: # 左键点击 self.on_pet_clicked() elif event.button == 3: # 右键点击,可以设计为特殊互动,如喂食 self.on_pet_right_clicked() elif event.type == pygame.MOUSEBUTTONUP: if event.button == 1: self.dragging = False elif event.type == pygame.MOUSEMOTION: if self.dragging: # 拖拽逻辑已在update中由鼠标位置更新 pass def on_pet_clicked(self): if self.state == PetState.SLEEPING: # 睡觉时轻点无效,需要“用力摇醒” self.shake_to_wake_up_counter += 1 if self.shake_to_wake_up_counter > 5: # 快速点击5次唤醒 self.mood_value = 30 self.change_state(PetState.IDLE) else: self.mood_value = min(100, self.mood_value + 10) # 增加心情值,上限100 self.dragging = True # 点击后可以拖拽 def update_state_logic(self, dt): # 心情值自然衰减 self.mood_value = max(0, self.mood_value - dt) # dt约为1/60秒 # 状态转换判断 self.state_timer += dt if self.state == PetState.IDLE: if self.mood_value > 60: self.change_state(PetState.HAPPY) elif self.mood_value < 15 and self.state_timer > 10: self.change_state(PetState.SLEEPY) elif self.state == PetState.HAPPY: if self.mood_value < 40: self.change_state(PetState.IDLE) elif self.state_timer > 5 and self.mood_value > 70: # 有概率进入撒娇状态 if random.random() < 0.3: self.change_state(PetState.ACTING_CUTE) elif self.state == PetState.ACTING_CUTE: if self.state_timer > 3: # 撒娇动作持续3秒 self.change_state(PetState.HAPPY) # ... 其他状态转换判断 def change_state(self, new_state): if self.state != new_state: self.state = new_state self.state_timer = 0 self.current_frame_index = 0 # 切换到新状态,动画从头开始 # 可以在这里播放状态切换音效 print(f“状态切换: {self.state.value}”) # 调试用 def update_animation(self, dt): self.animation_timer += dt if self.animation_timer >= self.frame_duration: self.animation_timer = 0 frames_list = self.frames.get(self.state, self.frames[PetState.IDLE]) self.current_frame_index = (self.current_frame_index + 1) % len(frames_list) self.image = frames_list[self.current_frame_index] def main(): pet = DesktopPet(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2) all_sprites = pygame.sprite.Group(pet) running = True while running: dt = clock.tick(FPS) / 1000.0 # 将tick返回的毫秒转换为秒 events = pygame.event.get() for event in events: if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: # 按ESC退出 running = False # 清屏,这里可以用一张背景图,或者透明背景(需要设置屏幕为支持alpha通道) screen.fill((0, 0, 0, 0)) # 透明黑色背景 # 更新和绘制所有精灵 all_sprites.update(dt, events) all_sprites.draw(screen) # 绘制心情值等调试信息(正式版可隐藏) font = pygame.font.SysFont(None, 24) mood_text = font.render(f‘Mood: {int(pet.mood_value)}’, True, (255, 255, 255)) screen.blit(mood_text, (10, 10)) pygame.display.flip() pygame.quit() sys.exit() if __name__ == “__main__”: main()

这段代码搭建了一个完整的骨架。DesktopPet类封装了宠物的所有属性和行为。主循环以60FPS运行,每一帧都处理事件、更新宠物状态和动画、重绘屏幕。dt(delta time)的引入是关键,它使得动画和计时逻辑与帧率解耦,无论电脑快慢,宠物的动作速度都是真实的1秒,而不是60帧。

3.2 “撒娇”行为的强化实现

“撒娇”是本项目的亮点,不能只是一个简单的动画循环。我们需要让它更有表现力。这里可以加入几个层次:

  1. 复合动画:撒娇不是单一动作。可以设计成:先抬头望向你 -> 然后打滚 -> 最后举起爪子。这需要在ACTING_CUTE状态内部再做一个子状态机,或者通过一个动画序列列表来控制。
  2. 粒子效果:在撒娇时,在宠物周围随机生成并飘起小心心(粒子系统)。这能极大增强视觉效果。
  3. 交互反馈:在撒娇状态下,如果用户点击宠物,应该有更强烈的正面反馈,比如心情值额外增加,或者播放一个特别的“嘤嘤”音效。

下面是一个简化版的心心粒子效果实现思路:

class HeartParticle: def __init__(self, x, y): self.x = x self.y = y self.vx = random.uniform(-20, 20) # 水平速度 self.vy = random.uniform(-30, -10) # 向上速度 self.lifetime = 1.0 # 存活1秒 self.age = 0.0 self.image = resources[‘images’][‘heart_bubble’] # 小心心图片 def update(self, dt): self.age += dt self.vy += 50 * dt # 模拟重力 self.x += self.vx * dt self.y += self.vy * dt def is_alive(self): return self.age < self.lifetime def draw(self, screen): alpha = int(255 * (1 - self.age / self.lifetime)) # 随年龄衰减透明度 self.image.set_alpha(alpha) screen.blit(self.image, (self.x, self.y)) # 在 DesktopPet 的撒娇状态中 if self.state == PetState.ACTING_CUTE: # 每隔0.2秒生成一个小心心 if self.state_timer - last_heart_spawn_time > 0.2: heart_x = self.rect.centerx + random.randint(-20, 20) heart_y = self.rect.top particle_system.add(HeartParticle(heart_x, heart_y)) last_heart_spawn_time = self.state_timer

3.3 无边框窗口与桌面交互优化

为了让宠物真正像桌面宠物,我们需要做两件事:无边框窗口窗口置顶

  • pygame.display.set_mode((width, height), pygame.NOFRAME)可以创建无边框窗口。
  • 窗口置顶需要调用操作系统API,在Windows上可以使用ctypes库调用user32.dll
import ctypes import ctypes.wintypes # 在创建屏幕后,设置窗口置顶 def set_window_topmost(hwnd): try: ctypes.windll.user32.SetWindowPos(hwnd, -1, 0, 0, 0, 0, 0x0001 | 0x0002) except Exception as e: print(f“设置窗口置顶失败: {e}”) # 获取当前pygame窗口的句柄 hwnd = pygame.display.get_wm_info()[‘window’] set_window_topmost(hwnd)

实操心得:无边框窗口带来一个问题:无法移动。所以我们之前实现了点击拖拽逻辑 (self.dragging = True)。但更友好的做法是,只有按住宠物身体部分拖拽才移动,点击其他区域(比如弹出菜单)则不触发拖拽。这需要精确的碰撞检测和事件处理。

4. 素材准备与项目美化

程序逻辑是骨架,素材才是血肉。一个可爱的宠物形象和恰当的音效能让项目质感提升好几个档次。

4.1 美术素材制作与处理

对于个人开发者,有几种获取素材的途径:

  1. 自己绘制:使用 Aseprite, Piskel 或 Photoshop 等工具。建议尺寸不要太大,64x64 到 128x128 像素即可,风格保持一致。
  2. 使用开源资源:在 itch.io, OpenGameArt.org 等网站搜索 “free sprite sheet”,注意遵守对应的开源协议(如 CC0, CC-BY)。
  3. 使用AI生成:利用 Stable Diffusion 等工具,通过“pixel art, cute cat, sprite sheet, 8 frames”这类提示词生成序列帧,再进行后期裁剪对齐。

处理技巧

  • 去背景:确保你的精灵图片背景是透明的(PNG格式,Alpha通道)。可以使用在线工具或Photoshop的魔棒工具。
  • 统一尺寸:将所有状态的动画帧放在一张大图上(精灵图),或者确保每个单独的PNG文件尺寸一致,方便代码中计算位置。
  • 命名规范:按状态_序号.png的方式命名,如pet_idle_0.png,pet_acting_cute_3.png,便于程序化加载。

4.2 音效与背景音乐

音效宜短促、清晰。可以在 freesound.org 上搜索 “cat meow”, “positive chime” 等关键词。背景音乐可选轻柔的循环音乐,但要注意在代码中控制音量,避免干扰用户。

在Pygame中加载和播放音效:

chime_sound = pygame.mixer.Sound(‘chime.wav’) chime_sound.set_volume(0.3) # 设置音量为30% chime_sound.play() # 播放一次

注意事项:音效播放不要太频繁,尤其是在循环中。比如“高兴”状态的背景呼噜声,应该判断是否已经在播放,避免重叠造成噪音。可以使用pygame.mixer.Channel来管理不同的音效通道。

5. 高级功能扩展与性能优化

基础版本完成后,你可以考虑加入更多让宠物更“智能”或更实用的功能。

5.1 状态持久化与记忆功能

让宠物记住上次关闭时的状态。可以使用json模块将宠物的心情值、位置等信息保存到本地文件,下次启动时读取。

import json def save_pet_data(pet): data = { ‘mood’: pet.mood_value, ‘x’: pet.rect.centerx, ‘y’: pet.rect.centery, # ... 其他需要保存的属性 } with open(‘pet_data.json’, ‘w’) as f: json.dump(data, f) def load_pet_data(pet): try: with open(‘pet_data.json’, ‘r’) as f: data = json.load(f) pet.mood_value = data.get(‘mood’, 50) pet.rect.center = (data.get(‘x’, 400), data.get(‘y’, 300)) except FileNotFoundError: pass # 第一次运行,使用默认值

5.2 系统托盘集成与后台运行

真正的桌面宠物应该能在后台运行,点击关闭按钮时最小化到系统托盘,而不是直接退出。这需要用到pystrayinfi.systray等第三方库。集成后,宠物可以安静地待在托盘区,右键菜单提供“退出”、“显示/隐藏”等选项。

5.3 性能优化要点

虽然这个项目不复杂,但良好的习惯对任何项目都有益:

  • 图像转换:在加载图片后,立即调用convert()convert_alpha()。这会将图片转换为与当前显示模式相同的格式,大幅提升后续blit操作的速度。
    image = pygame.image.load(‘file.png’).convert_alpha()
  • 脏矩形更新:如果宠物是屏幕上唯一变化的部分,可以使用pygame.display.update(rect_list)只更新发生变化区域,而不是整个屏幕 (pygame.display.flip()),这在复杂场景中能提升性能。
  • 避免在循环中加载资源:所有图片、音效都应在初始化时加载好。

6. 常见问题与调试技巧实录

在实际开发中,你几乎一定会遇到下面这些问题。这里是我的踩坑记录和解决方案。

6.1 Pygame安装失败与环境配置

这是新手第一道坎。错误信息常包含Microsoft Visual C++ 14.0 is required

解决方案

  1. 最佳实践:使用condapip安装预编译的轮子 (wheel)。
    # 首先升级pip python -m pip install --upgrade pip # 尝试安装预编译的pygame pip install pygame --pre # 或者指定版本 pip install pygame==2.5.2
  2. 安装构建工具:如果必须从源码编译,请安装 Microsoft C++ 生成工具。访问Visual Studio官网,下载“Visual Studio Build Tools”,安装时勾选“C++ 生成工具”。
  3. 使用集成环境:对于纯粹的学习者,直接使用 Anaconda 或 PyCharm 这类IDE,它们通常能更好地管理环境和依赖。

6.2 透明背景与图像叠加问题

想让宠物浮在桌面其他窗口之上,需要窗口支持透明。设置屏幕模式时使用pygame.SRCALPHA

# 创建一个支持透明通道的屏幕 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.NOFRAME | pygame.SRCALPHA) # 然后用透明色清屏 screen.fill((0, 0, 0, 0)) # RGBA,A=0为全透明

常见坑:如果你加载的图片本身有透明背景,但显示出来却有黑色方块,大概率是因为你没有使用convert_alpha()方法。

6.3 宠物状态逻辑“抽搐”或行为异常

这通常是状态转换条件设置不合理,导致状态在边界反复横跳。

调试方法

  1. 打印日志:在change_state函数中加入打印语句,实时观察状态和心情值的变化。
  2. 可视化调试:像基础代码里做的那样,在屏幕上实时渲染出心情值、当前状态名。
  3. 引入“状态冷却”:对于一些状态,转换出去后,即使条件立刻再次满足,也强制等待一个最短持续时间。例如,刚从“撒娇”状态出来,5秒内不能再进入“撒娇”,避免频繁撒娇显得廉价。
def change_state(self, new_state): if self.state != new_state and not self.state_cooldown: # ... 状态切换逻辑 self.state_cooldown = 2.0 # 进入新状态后,设置2秒冷却 def update_state_logic(self, dt): # 更新冷却时间 if self.state_cooldown > 0: self.state_cooldown -= dt # ... 原有的状态判断逻辑

6.4 打包成可执行文件 (EXE)

使用PyInstaller是首选。在项目根目录下执行:

pip install pyinstaller pyinstaller -w -F --add-data “resources;resources” main.py
  • -w: 隐藏命令行窗口。
  • -F: 打包成单个exe文件。
  • --add-data “resources;resources”: 将resources文件夹(存放图片音效)打包进去。注意分号前是源路径,分号后是exe内的虚拟路径(Windows用分号;,Linux/Mac用冒号:)。

打包后资源路径问题:代码中加载资源不能再用相对路径‘pet_idle_0.png’,需要使用sys._MEIPASS来获取打包后的临时资源路径。

def resource_path(relative_path): “”“获取资源的绝对路径。打包成exe后也能正确找到资源。”“” try: base_path = sys._MEIPASS # PyInstaller创建的临时文件夹 except AttributeError: base_path = os.path.abspath(“.”) return os.path.join(base_path, relative_path) # 加载资源时 image = pygame.image.load(resource_path(‘resources/pet_idle_0.png’)).convert_alpha()

7. 完整源码结构与使用指南

我将提供一个精简但功能完整的项目结构,包含了上述讨论的核心功能。你可以在此基础上自由发挥。

项目目录结构

my_desktop_pet/ ├── main.py # 主程序入口 ├── pet.py # DesktopPet 类定义 ├── particle.py # 粒子效果类(如心心) ├── utils.py # 工具函数(资源加载、路径处理) ├── requirements.txt # 项目依赖 ├── resources/ # 资源文件夹 │ ├── images/ │ │ ├── pet_idle_0.png │ │ ├── pet_idle_1.png │ │ ├── pet_happy_0.png │ │ └── ... │ └── sounds/ │ ├── meow.wav │ └── purr.wav └── README.md # 项目说明

requirements.txt内容

pygame==2.5.2

快速开始

  1. 确保已安装Python(3.7以上)。
  2. 在项目目录下,运行pip install -r requirements.txt
  3. 准备你的素材,放入resources/images/resources/sounds/目录,并按照代码中的命名约定命名(或修改代码中的加载逻辑)。
  4. 运行python main.py

核心源码 (pet.py关键部分增强版)

import pygame import random from enum import Enum from utils import resource_path, load_images class PetState(Enum): IDLE = “idle” HAPPY = “happy” ACTING_CUTE = “acting_cute” SLEEPY = “sleepy” SLEEPING = “sleeping” class DesktopPet(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.state = PetState.IDLE self.mood = 50.0 self.state_timer = 0.0 self.animation_timer = 0.0 self.frame_duration = 0.15 self.current_frame = 0 self.dragging = False self.shake_counter = 0 self.last_shake_time = 0 # 加载资源 self.frames = self._load_all_frames() self.image = self.frames[self.state][self.current_frame] self.rect = self.image.get_rect(center=(x, y)) # 撒娇粒子效果相关 self.hearts = [] self.heart_spawn_timer = 0.0 def _load_all_frames(self): frames = {} base_path = “resources/images” # 假设每种状态有4帧,命名为 state_0.png, state_1.png ... for state in PetState: state_frames = [] for i in range(4): # 这里需要根据你实际的帧数修改 try: path = resource_path(f“{base_path}/{state.value}_{i}.png”) img = pygame.image.load(path).convert_alpha() state_frames.append(img) except FileNotFoundError: # 如果找不到图片,用默认色块代替,方便调试 print(f“警告: 未找到图片 {state.value}_{i}.png”) img = pygame.Surface((64, 64), pygame.SRCALPHA) color_map = {PetState.IDLE: (200,200,200), PetState.HAPPY: (255,255,150), PetState.ACTING_CUTE: (255,150,150), PetState.SLEEPY: (150,150,255), PetState.SLEEPING: (100,100,100)} img.fill(color_map.get(state, (255,255,255))) state_frames.append(img) frames[state] = state_frames return frames def update(self, dt, events): self._handle_input(events) self._update_mood(dt) self._update_state(dt) self._update_animation(dt) self._update_hearts(dt) # 更新粒子效果 if self.dragging: self.rect.center = pygame.mouse.get_pos() def _handle_input(self, events): for event in events: if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: if self.rect.collidepoint(event.pos): self._on_clicked() elif event.type == pygame.MOUSEBUTTONUP and event.button == 1: self.dragging = False self.shake_counter = 0 def _on_clicked(self): current_time = pygame.time.get_ticks() if self.state == PetState.SLEEPING: # 快速点击唤醒逻辑 if current_time - self.last_shake_time < 500: # 500毫秒内 self.shake_counter += 1 else: self.shake_counter = 1 self.last_shake_time = current_time if self.shake_counter >= 3: self.mood = 40 self._change_state(PetState.IDLE) print(“被摇醒啦!”) else: self.mood = min(100, self.mood + 12) self.dragging = True # 播放被点击音效 (此处省略音效加载代码) def _update_mood(self, dt): # 不同状态下心情衰减速度不同 decay_rate = 1.0 # 默认 if self.state == PetState.SLEEPING: decay_rate = 0.2 # 睡觉时心情变化慢 elif self.state == PetState.HAPPY or self.state == PetState.ACTING_CUTE: decay_rate = 0.5 # 开心时心情维持更久 self.mood = max(0, self.mood - decay_rate * dt) def _update_state(self, dt): self.state_timer += dt old_state = self.state if self.state == PetState.IDLE: if self.mood > 65: self._change_state(PetState.HAPPY) elif self.mood < 20 and self.state_timer > 8: self._change_state(PetState.SLEEPY) elif self.state == PetState.HAPPY: if self.mood < 45: self._change_state(PetState.IDLE) elif self.state_timer > 4 and self.mood > 75 and random.random() < 0.25: self._change_state(PetState.ACTING_CUTE) elif self.state == PetState.ACTING_CUTE: if self.state_timer > 3.5: self._change_state(PetState.HAPPY) elif self.state == PetState.SLEEPY: if self.mood > 30: self._change_state(PetState.IDLE) elif self.state_timer > 6: self._change_state(PetState.SLEEPING) elif self.state == PetState.SLEEPING: if self.mood > 50: # 被摇醒的逻辑在_on_clicked里处理了 self._change_state(PetState.IDLE) if old_state != self.state: self.state_timer = 0 self.current_frame = 0 def _change_state(self, new_state): if self.state != new_state: self.state = new_state print(f“状态变更 -> {self.state.value}”) def _update_animation(self, dt): self.animation_timer += dt if self.animation_timer >= self.frame_duration: self.animation_timer = 0 state_frames = self.frames.get(self.state, self.frames[PetState.IDLE]) self.current_frame = (self.current_frame + 1) % len(state_frames) self.image = state_frames[self.current_frame] def _update_hearts(self, dt): # 只在撒娇时生成小心心 if self.state == PetState.ACTING_CUTE: self.heart_spawn_timer += dt if self.heart_spawn_timer > 0.15: self.heart_spawn_timer = 0 # 在宠物上方随机位置生成一个小心心粒子 heart_x = self.rect.centerx + random.randint(-25, 25) heart_y = self.rect.top - 10 self.hearts.append([heart_x, heart_y, 0, random.uniform(-2, 2), random.uniform(-3, -1), 1.0]) # [x, y, 年龄, vx, vy, 生命值] # 更新所有小心心的状态 new_hearts = [] for heart in self.hearts: heart[0] += heart[3] # x += vx heart[1] += heart[4] # y += vy heart[4] += 9.8 * dt # 模拟重力 heart[2] += dt # 年龄增加 heart[5] = max(0, 1.0 - heart[2] / 1.5) # 生命值随时间衰减 if heart[5] > 0: new_hearts.append(heart) self.hearts = new_hearts def draw_hearts(self, screen): # 绘制所有小心心 for heart in self.hearts: x, y, age, vx, vy, life = heart # 这里可以加载一个小心心图片,这里用绘制一个粉色矩形代替 heart_surf = pygame.Surface((10, 10), pygame.SRCALPHA) pygame.draw.polygon(heart_surf, (255, 100, 150, int(255*life)), [(5,0), (0,5), (5,10), (10,5)]) screen.blit(heart_surf, (int(x-5), int(y-5)))

这个增强版的DesktopPet类已经具备了完整的核心逻辑:状态管理、心情系统、动画更新、点击交互以及一个简单的粒子系统(用于撒娇时的小心心)。将它与之前的主循环代码结合,一个会撒娇、有情绪的智能桌面宠物就初具雏形了。剩下的工作就是寻找或绘制一套精美的精灵图,调整状态转换的参数让它行为更自然,然后享受你的创作成果吧。