Spoof for love
如果你还在为表白失败而伤心,担心害怕被拒绝而失落,亦或者因为害羞而迟迟不敢表露心意,那么下面这一款软件很适合正在发愁的你。
软件效果
-
程序开始(此时应有背景音乐)
-
移动鼠标到
Fuck off
按钮(自动逃跑)
-
没有办法,只有点击
I do
(手动滑稽)
如何制作
当然我并不是在推销这种产品,这里我们更重要的是介绍如何编写这款程序。
前期准备
- 安装python3,熟悉最基础的语法
- 安装第三方包:pygame
pip install pygame
- 准备一些好看的字体、动听的音乐、漂亮的图片
font: AlexaStd.otf
music: SakuraTears.mp3
image:
你可以在网上直接搜索下载这些资源,亦可以在文末的github链接中找到所有资源,包括源代码。
程序流程
1.导入包
23 import sys
24 import random
25
26 import pygame
27 from pygame.locals import *
28
29 from constant import *
sys模块用于游戏退出,random模块用于生成随机数,pygame模块包含了众多用于游戏开发的API,pygame.locals导入pygame模块中的常量,constant是自己编写的包含本程序常量的模块。
2.主函数
- 屏幕初始化和元素准备
63 def main():
64 """Run the logic in here."""
65 # init window
66 pygame.init()
67 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
68 pygame.display.set_caption(SCREEN_TITLE)
69 fps_clock = pygame.time.Clock()
70
init window
我们调用pygame.init初始化所有pygame模块,然后使用pygame.display模块分别调用set_mode和set_caption函数设置screen大小和标题,这也就是后面将要显示的游戏主屏幕。SCREEN_WIDTH和 SCREEN_HEIGHT是constant.py文件中编写好的常量,后面的大写变量均如此(除QUIT, MOUSEBUTTONDOWN,MOUSEMOTION为pygame.locals模块中的常量)。最后使用pygame.time模块调用Clock,获取此对象,后面将用此对象设置屏幕刷新率即fps。
fps值越大,动画在每一秒中刷新的帧就越多,游戏运行的越流畅,实际上表现为动画的帧画面更新的越快,如果不设置此值,将会用电脑最快的速度执行。此时fps非常高,对于某些动作可能会快到你无法察觉,当然对本程序影响不大,但是忽略fps的设置会导致你的电脑cpu占用率会非常高。
71 # play music
72 pygame.mixer.music.load(MUSIC)
73 pygame.mixer.music.play(MUSIC_LOOP_TIME, MUSIC_START_TIME)
74 pygame.mixer.music.set_volume(MUSIC_VOLUME)
75
play music
使用pygame.mixer.music模块,load加载源文件,play开始播放,set_volume设置音量大小。MUSIC_LOOP_TIME常量值为-1,表示一直循环;MUSIC_START_TIME为音乐播放的起点位置,为float类型。
76 # load image
77 img_sf = pygame.image.load(IMAGE).convert()
78 img_rect = img_sf.get_rect()
79 img_rect.midtop = (HALF_WIDTH, SCREEN_MARGIN)
80
load image
使用pygame.image模块,通过load加载图片源文件,调用convert转换格式,使得后面调用blit(将图片“画”到screen上,可以理解为复制)更快。此函数调用会返回一个surface对象,存储在img_sf中;调用surface对象的get_rect获取此对象的位置(默认从游戏主屏幕screen坐标(0,0)开始,对应的是左上角点的位置),会返回一个rect对象,存储在img_rect中,然后给rect对象属性midtop赋值,确定其位置。关于常量值可以后面具体看我的constant.py文件。
81 # say something
82 text_pos = (HALF_WIDTH, img_rect.bottom + (FONT_SIZE + SCREEN_MARGIN) / 2)
83
say something
使用常量值创建文本对象位置的元组,存储在text_pos中,后面会将此位置赋值给rect对象属性center,即此坐标为文本中心点的坐标(文本也可以视为一个矩形图像,本身含有背景,文字+背景=矩形,后面会有详细说明)。
84 # put button
85 l_but_x = QUARTER_WIDTH - BUTTON_WIDTH / 2
86 b_but_x = HALF_WIDTH + l_but_x
87 but_y = SCREEN_HEIGHT - BUTTON_HEIGHT - SCREEN_MARGIN
88 lucky_button_rect = pygame.Rect(l_but_x, but_y,
89 BUTTON_WIDTH, BUTTON_HEIGHT)
90 bad_button_rect = pygame.Rect(b_but_x, but_y,
91 BUTTON_WIDTH, BUTTON_HEIGHT)
92
put button
使用常量值创建两个按钮:lucky_button和bad_button,准确的说是确定两个按钮位置,lucky_button可以获取点击进入最终页面,bad_button一旦鼠标移动到上面就会自动逃跑。pygame.Rect模块生成rect对象,参数分别为图像左上角坐标x,y和图像自身宽和高width,height。
- 游戏循环
93 # main loop
94 accept = False
95 while True:
96 screen.fill(PINK)
97
98 if accept:
99 ct_text(THE_LAST_SURFACE_TEXT, screen, WHITE, CENTER, FONT_SIZE)
100 for event in pygame.event.get():
101 # when she accept it, the window is allowed to close!
102 if event.type == QUIT:
103 pygame.quit()
104 sys.exit()
105 else:
106 screen.blit(img_sf, img_rect)
107 ct_button(LUCKY_TEXT, screen, PALEPINK, lucky_button_rect)
108 ct_button(BAD_TEXT, screen, PALEPINK, bad_button_rect)
109 ct_text(THE_FIRST_SURFACE_TEXT, screen, WHITE, text_pos, FONT_SIZE)
110
111 for event in pygame.event.get():
112 # can't close the window
113 if event.type == MOUSEMOTION:
114 pos = mv_button(bad_button_rect)
115 if pos[0]:
116 bad_button_rect.x = pos[0]
117 bad_button_rect.y = pos[1]
118 elif event.type == MOUSEBUTTONDOWN:
119 x, y = pygame.mouse.get_pos()
120 if lucky_button_rect.collidepoint(x, y):
121 accept = True
122
123 pygame.display.update()
124 fps_clock.tick(FPS)
main loop
一开始设置accept为False,毕竟对方还没有接受你。每次循环都调用screen.fill重新填充屏幕,覆盖之前的图像。简单说明一下,游戏的每一帧加起来连续播放构成动画,fps即代表播放的速度,我们需要每次循环重新生成新的一帧图像,因为bad_button会移动,所以需要覆盖之前的图像,否则图像上会留下上次bad_button的图像和已经移动后的bad_button,同时由于没有给游戏主屏幕screen填充颜色,此时screen会呈现为黑色(你可以通过注释本行代码来进行验证)。覆盖图像并不会影响啥,不会造成我们所理解的堆积从而带来麻烦,其实理解为重新绘制更好些,每次循环开始都要重新绘制图像,如果不这样做那么只是呈现出以前画好的画面而已。在游戏中我们需要绘制每一帧的图像,因为每一帧图像毕竟是不同的,我们希望通过每一帧的快速播放来形成动画。只不过如果每一帧的图像信息量比较大,电脑的cpu忙不过来会导致fps降低,从而带来我们在视觉上的卡顿。
如果accept为True,执行ct_text在屏幕上写上表达自己心情激动的文字,此时可以捕获QUIT信号即点击程序右上角的×可以退出程序;否则调用blit将image复制到游戏主屏幕screen;同时调用ct_button创建两个按钮,使用ct_text写上求爱誓言,此时不处理QUIT信号,仅判断鼠标的移动和点击,那个你心目中的她不答应你无法正常退出此程序。pygame.event.get会获取所有发生的事件,返回一个待处理的事件队列,调用event.type判断事件类型:MOUSEMOTION为鼠标移动,MOUSEBUTTONDOWN为鼠标点击,即按下。若鼠标移动,调用mv_button判断是否移动到bad_button,若是返回随机位置存储在pos中;若鼠标按下,调用pygame.mouse.get_pos获取点击位置坐标x,y,调用碰撞检测函数collidepoint判断是否在lucky_button位置,若是设置accept为True。其中ct_text,ct_button,mv_button为我自己编写的函数,后面将作介绍。
最后最关键的是调用pygame.display.update,因为我们除了对游戏主屏幕screen的设置之外,之前的所有操作都是在内存中进行的,我们需要使用update来将在内存逻辑中复制上去的图片,写上去的文字在显示器上呈现出来。当然update的用法很关键,很多时候fps的降低都是因为update的使用不当造成的,我在这里不再赘述。然后在循环的末尾调用Clock对象(存储在fps_clock)的tick方法设置fps值。
3.函数介绍
32 def ct_text(content, screen, color, pos, size):
33 """Create the text."""
34 text = pygame.font.Font(FONT, size)
35 text_render = text.render(content, True, color)
36 text_rect = text_render.get_rect()
37 text_rect.center = pos
38 screen.blit(text_render, text_rect)
39
40
ct_text
参数:文字内容,游戏主屏幕,文字颜色,位置(元组表示的点坐标),字体大小
使用pygame.font模块,将字体路径和字体大小传入Font函数,返回Font对象;然后调用render方法将文字绘制在一个新的surface对象上并返回此对象,其中第二个参数为True代表抗锯齿,让文字边缘更平滑。pygame不能直接写文字在主屏幕上,但是通过这种方法,创建新的surface对象,就像一个空白的image画卷,然后将文字画在其上面。之后同图片复制到主屏幕的操作一样,利用get_rect获取rect对象,将pos赋值给rect对象center属性(center代表图像的中心点坐标),然后blit。
41 def ct_button(title, screen, color, rect, thickness=0):
42 """Create button."""
43 pygame.draw.rect(screen, color, rect, thickness)
44 ct_text(title, screen, WHITE, rect.center, FONT_SIZE)
45
46
ct_button
参数:按钮标题,游戏主屏幕,按钮颜色,位置(为rect对象(x,y,width,height)),边框厚度
使用pygame.draw模块,调用rect绘制一个矩形,thickness是矩形边框的厚度大小,默认为0,即填充式绘制--整个按钮都会被color对应的颜色充满;如果thickness不为0,那么只是边框为color对应的颜色,按钮中间部分为主屏幕对应的颜色,即只绘制了边框。然后我们使用ct_text为按钮写上title。注意传递给pos参数的是rect.center属性值,这个值是一个元组表示的点坐标。
47 def get_random_pos():
48 """Generate random position."""
49 x = random.randint(0, SCREEN_WIDTH - BUTTON_WIDTH)
50 y = random.randint(0, SCREEN_HEIGHT - BUTTON_HEIGHT)
51 return x, y
52
53
get_random_pos
没有参数,很简单,返回在0和屏幕宽度减去按钮宽度之间的x坐标,在0和屏幕高度减去按钮高度之间的y坐标。因为绘制图像总是从图像的左上角(x,y)点坐标开始绘制,默认从屏幕的(0, 0)开始,然后按照相应的宽度和高度进行绘制。
54 def mv_button(rect):
55 """Move button randomly."""
56 x, y = pygame.mouse.get_pos()
57 if rect.collidepoint(x, y):
58 new_x, new_y = get_random_pos()
59 return (new_x, new_y)
60 return (None, None)
61
62
mv_button
参数:rect对象--图像的位置
调用pygame.mouse.get_pos获取鼠标位置,然后使用rect对象的collidepoint方法检测是否存在碰撞,即是否鼠标移动到了此位置。若是,返回随机的新位置,否则,返回None构成的元组。
常量值py文件
下面的常量值基本上都能知其意会其形,我就不再赘述了,也不建议你都看一遍,只需要知道所使用到的值。当然下面有很多值我都并没有使用,只是为了方便代码迭代一并列出,这也是为什么我要单独使用一个constant.py文件来存储这些值。
# -*- coding: utf-8 -*-
"""
Constant file.
include the screen parameters, special point coords,
data file path, the RGB's order of color and more.
you can alter some parameters make sure it work like you want.
but be careful for do that, it may cause some interesting bugs.
"""
import sys
from os import path
FPS = 60
SCREEN_WIDTH = 680
SCREEN_HEIGHT = 540
SCREEN_MARGIN = 20
SCREEN_TITLE = 'Spoof for love!'
BUTTON_WIDTH = 100
BUTTON_HEIGHT = 50
assert (BUTTON_WIDTH * 4 < SCREEN_WIDTH) and \
(BUTTON_HEIGHT * 4 < SCREEN_HEIGHT), \
"please make sure the button small or screen big!"
HALF_WIDTH = SCREEN_WIDTH / 2
HALF_HEIGHT = SCREEN_HEIGHT / 2
QUARTER_WIDTH = SCREEN_WIDTH / 4
QUARTER_HEIGHT = SCREEN_HEIGHT / 4
CENTER = (SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2)
LEFT_TOP_QUARTER = (SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4)
LEFT_BOTTOM_QUARTER = (SCREEN_WIDTH / 4, CENTER[1] + SCREEN_HEIGHT / 4)
RIGHT_TOP_QUARTER = (CENTER[0] + SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4)
RIGHT_BOTTOM_QUARTER = (CENTER[0] + SCREEN_WIDTH / 4,
CENTER[1] + SCREEN_HEIGHT / 4)
# if program is pack, sys will have 'frozen' attr,
# use sys._MEIPASS as path
if getattr(sys, 'frozen', False):
d_path = sys._MEIPASS
else:
d_path = path.dirname(path.abspath(__file__))
FONT = path.join(d_path, 'fonts', 'AlexaStd.otf')
IMAGE = path.join(d_path, 'images', 'propose.jpg')
MUSIC = path.join(d_path, 'music', 'SakuraTears.mp3')
# RGB's order of color
# choose what color of you like
PINK = (255, 128, 128)
PALEPINK = (255, 192, 203)
GRAY = (100, 100, 100)
NAVYBLUE = (60, 60, 100)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
ORANGE = (255, 128, 0)
PURPLE = (255, 0, 255)
CYAN = (0, 255, 255)
MUSIC_LOOP_TIME = -1 # -1 means always play
MUSIC_START_TIME = 0.0 # the song play from where
MUSIC_VOLUME = 0.25 # the song's volume
LUCKY_TEXT = 'I do'
BAD_TEXT = 'Fuck off'
FONT_SIZE = 30
THE_FIRST_SURFACE_TEXT = 'Would you marry me?'
THE_LAST_SURFACE_TEXT = 'I know you will accept it!'
最后的最后
如果你对pygame中的相关API仍充满疑惑或者想要了解更多,亦或者你想要此程序的exe文件(windows上可直接运行,不需要python环境),那么可以访问以下链接,我将不定期更新和完善!
更多精彩敬请访问github和关注我的其他内容!
pygame:pygame docs
github:Spoof_for_love
:我的主页