ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

一个 Python 新手,靠 AI 帮忙把贪吃蛇做成了“蛇娘“游戏(2/5):画面为什么不糊

一个 Python 新手,靠 AI 帮忙把贪吃蛇做成了“蛇娘“游戏(2/5):画面为什么不糊 系列《一个 Python 新手靠 AI 帮忙把贪吃蛇做成了蛇娘游戏》共 5 篇。本篇讲我为了画面清晰 操作跟手踩过的坑。上一篇讲了我怎么把代码分成五层。这一篇想聊点更看得见的东西——画面糊不糊、操作跟不跟手。不瞒大家在做这个游戏之前我连分辨率缩放这些词都只是听过、没真懂。我一开始天真地以为随便找个尺寸把画面画出来最后拉伸铺满窗口不就行了结果第一次跑起来画面糊得像打了马赛克角色边缘全是虚的。那一刻我才发现能显示和显示得清楚是两码事。这一篇就是我把这段从糊到清晰的过程记录下来中间还夹着两个卡了我好几晚的坑——一个是每切一次全屏窗口就诡异小一圈另一个是中文输入法把我的方向键吃了。一、画面为什么糊我一开始先画小再拉大的做法错了我最初的想法特别朴素反正大多数人屏幕是 1080p那我就固定按一个尺寸比如 960×540把画面画出来最后用pygame.transform.smoothscale拉伸到窗口大小铺满不就完事了结果就是前面说的——糊。尤其是文字和角色立绘的边缘放大之后全是模糊的过渡色看着特别廉价。我把截图发给 AI 问为什么这么糊它一句话点醒我你是在把一张小图放大放大的过程一定会丢细节、产生模糊正确的做法是反过来——直接按窗口的真实分辨率去画画完原样贴上去全程不缩放。这个思路我当时觉得挺反直觉不缩放怎么适配不同窗口但照着改完画面瞬间锐利了。核心其实就几行# 教学简化版·非完整可运行·by 【外收内放】 # 渲染表面 窗口的原生分辨率像素 1:1 self.render_surface pygame.Surface((self.window_width, self.window_height)) # ……所有场景都画在这张 render_surface 上…… # 关键原样贴到窗口不做任何 smoothscale 拉伸 self.display.blit(self.render_surface, (0, 0)) pygame.display.flip()我把这套做法记成了自己的一句话在多大的窗口上就画多大的图。后来我才知道这大概叫原生分辨率渲染但一开始我纯粹是被 AI 逼着改对的谈不上理解。那问题来了窗口有大有小我画按钮、画字体总不能每个尺寸都单独写一套吧这就引出了第二节——缩放因子也是这个项目里我觉得最值的一个设计。二、缩放因子 s()一套设计稿适配所有窗口先画小图会糊那直接按窗口尺寸画又带来一个新麻烦我在 1920 宽的窗口上把按钮画成 200 像素宽换到 1280 的窗口上这个按钮就显得特别大、布局全乱了。我又去问 AI它教了我一个特别简单的办法我给它起了个名字叫s()scale 的意思先定一个设计基准宽度我用的是DESIGN_WIDTH 1920——就是我画设计稿时脑子里的那个尺寸算一个缩放因子scale 当前窗口宽 / 1920以后所有尺寸字体大小、按钮宽高、间距、坐标都不要写死全部套一层s()让它按scale自动换算。# 教学简化版·非完整可运行·by 【外收内放】 DESIGN_WIDTH 1920 # 我画设计稿时的基准宽度 property def scale(self): return self.window_width / DESIGN_WIDTH # 窗口 1280 宽 → scale ≈ 0.67 def s(self, value): return int(value * self.scale) # 设计尺寸 → 当前窗口的真实像素用起来就是无脑套一层。我设计稿里写字体 28 号、按钮 200×60、离左边 40代码里就写# 教学简化版·非完整可运行·by 【外收内放】 font get_font(self.s(28)) # 窗口小字体自动跟着小 pygame.draw.rect(screen, color, (self.s(40), self.s(20), self.s(200), self.s(60)))三、坑一每切一次全屏窗口就诡异小一圈现象是这样的游戏里有个显示设置界面可以切分辨率和全屏。我发现每切一次窗口就小一圈切个五六次窗口缩成巴掌大跟闹鬼了一样。我一开始完全没头绪甚至怀疑是 pygame 的 bug。后来我把切换分辨率的那段代码贴给 AI它让我在每次set_mode之后把拿到的屏幕尺寸打印出来看看。我一打印真相大白——问题出在我用pygame.display.Info().current_w去取屏幕尺寸来做钳制。AI 解释说在窗口模式下Info().current_w返回的根本不是显示器分辨率而是当前这个窗口的尺寸。而我当时的逻辑是窗口不能超过屏幕的 95%写成了类似# 教学简化版·非完整可运行·by 【外收内放】 # ❌ 我最初的错误写法 sw pygame.display.Info().current_w # 窗口模式下这其实是当前窗口的宽 self.window_width min(target_w, int(sw * 0.95))于是每次切换都拿上一次的窗口尺寸再乘一个 0.95这个 0.95 会一次次叠加0.95、0.90、0.86……窗口当然越切越小。用 AI 的话说这叫复利式缩小。修正的关键是钳制基准必须用真实桌面分辨率而不是被当前窗口污染过的Info()。另外 AI 还提醒我窗口超出屏幕时应该整体等比缩小保持宽高比而不是宽、高各自单独min一下——那样会把画面压变形。改成这样就好了# 教学简化版·非完整可运行·by 【外收内放】 # ✅ 修正后 # ① 用 get_desktop_sizes() 拿真实桌面分辨率它不会被已创建的窗口污染 sw, sh pygame.display.get_desktop_sizes()[0] # ② 超出可用空间时按宽高比整体等比缩小fit 取最小的那个比例 avail_w, avail_h int(sw * 0.98), int(sh * 0.92) # 预留标题栏、任务栏 fit min(1.0, avail_w / target_w, avail_h / target_h) self.window_width int(target_w * fit) self.window_height int(target_h * fit)这个坑给我最大的教训是同一个 API 在不同模式下行为可能完全不同Info()在全屏和窗口下返回的东西就不一样。我以前默认取屏幕尺寸就一个写法吃了大亏。AI 还教我一句诊断技巧这种 bug 你在循环外只取一次值是看不出来的必须在真实的切换流程里一步步打印才能复现。四、坑二中文输入法把我的方向键吃了第二个坑更隐蔽一度让我怀疑自己的键盘坏了。现象游戏用 WASD 和方向键移动。我在自己电脑上测试时一切正常但有一次我用中文输入法打了几个字切回游戏后——方向键彻底失灵了怎么按角色都不动。重启游戏又能用一旦切过输入法就废。我百思不得其解把现象描述给 AI。它问我你是不是在 Windows 上、而且当时开着中文输入法 我说是。它就直接给出了答案Windows 的输入法比如搜狗会在系统消息这一层把键盘输入先截胡了pygame 的按键事件根本收不到。解决办法是调用一个 Windows API把窗口的输入法关联解除掉。就这么几行但对我这种没碰过ctypes的新手来说完全是知识盲区# 教学简化版·非完整可运行·by 【外收内放】 import ctypes hwnd pygame.display.get_wm_info()[window] # 拿到窗口句柄 ctypes.windll.imm32.ImmAssociateContext(hwnd, None) # 解除输入法关联 # ⚠️ 必须在 pygame.display.set_mode(...) 之后立刻调用加上这几行之后无论系统是什么输入法游戏里的方向键都稳如老狗再也没被截胡过。不过这里还有个我自己撞出来的小插曲解除输入法之后我发现存档命名的时候打不了中文了因为输入法被我禁用了嘛。这显然不行。于是我又让 AI 帮我把输入法做成可开关的——平时禁用保证操作等玩家要给存档起名字时临时把输入法恢复回来输完再关掉。我把它封装成了一个set_text_input(True / False)的小开关。这件事让我明白一个道理解决一个问题时别只顾着堵住还要想清楚会不会把正常的功能也一起堵了。我一开始就只想把输入法干掉完全没考虑到还需要打字的场景。五、大地图摄像机让世界比屏幕大操作还跟手前面都在讲清晰这一节讲跟手。上一篇说过我删掉了网格想做一张比屏幕大得多的地图。那问题就来了地图比屏幕大玩家跑到地图右边时画面总得跟着往右移吧这就是摄像机。我的做法又是 AI 教的是把坐标分成两套世界坐标整个大地图的坐标所有怪物、道具、玩家的位置都存在这套坐标里屏幕坐标真正画到窗口上的坐标。两者的换算简单到出乎我意料——就是减去摄像机当前看到的那个左上# 教学简化版·非完整可运行·by 【外收内放】 # 世界比屏幕大好几倍比如 3 屏宽、2 屏高 self.world_w WORLD_SCREENS_X * self.W self.world_h WORLD_SCREENS_Y * self.H # 画任何东西时世界坐标减去摄像机左上角 屏幕坐标 def wx(self, x): return x - self.cam[0] def wy(self, y): return y - self.cam[1]用一张图表示这套流程就是我现在脑子里的渲染流水线减去摄像机 cam画到1:1 blit 不缩放世界坐标所有实体的 pos屏幕坐标wx / wyrender_surface窗口原生分辨率游戏窗口这里我又踩了一个手感上的坑我一开始让摄像机硬贴着玩家玩家一动画面立刻同步平移。结果就是画面晃得我头晕尤其是快速转向的时候整个世界都在猛甩。AI 说这是因为缺少缓冲教我用了线性插值我当时连插值是什么都不知道摄像机不是瞬间到位而是每一帧朝目标位置挪一点点这样画面就有了平滑的跟随感。核心就这几行但那个math.exp我是后来才慢慢品出味道的# 教学简化版·非完整可运行·by 【外收内放】 def _update_cam(self, dt): # 目标让玩家待在屏幕正中央 tx self.snake.pos[0] - self.W / 2 ty self.snake.pos[1] - self.H / 2 # k 控制跟得多紧用 exp 是为了让不同帧率下手感一致这点我很久后才懂 k 1.0 - math.exp(-CAM_LERP * dt) self.cam[0] (tx - self.cam[0]) * k # 每帧朝目标挪 k 的比例 self.cam[1] (ty - self.cam[1]) * k self._clamp_cam() # 别越过世界边界否则会露出黑边最后那个_clamp_cam()也是撞坑撞出来的玩家靠近地图边缘时如果不限制摄像机画面就会移出世界、露出一片黑边。所以要把摄像机死死钳在世界范围内。这种边界情况我这个新手基本都要亲自撞一次才记得住。六、附带一提全屏黑边和 DPI还有一个小坑顺便记一下免得有人和我一样困惑。我在某些开了显示缩放比如 125%、150%的 Windows 电脑上切全屏会发现四周有一圈黑边画面铺不满。查了半天AI 告诉我这是因为程序没声明DPI 感知Windows 偷偷帮我们做了一层缩放。解决办法是在初始化 pygame 之前先声明一下# 教学简化版·非完整可运行·by 【外收内放】 import ctypes # 必须在 pygame.init() 之前调用否则不生效 ctypes.windll.shcore.SetProcessDpiAwareness(2) # 2 Per-Monitor V2这行代码我也是似懂非懂地抄上去的但确实管用。对新手来说ctypes.windll.xxx这种直接调 Windows 系统 API 的东西看着很唬人其实很多都是知道有这么个调用就能解决问题——而这正是有 AI 陪着查的最大好处。
返回列表