【Python + pygame】独立游戏 - 【战术井字棋 (tic-tactics)】(鱼币)
本帖最后由 cjjJasonchen 于 2025-7-24 08:01 编辑战术井字棋 (tic-tactics) - 简介
各位新鱼油,老鱼油们大家好啊~
我是 cjjJasonchen~
这次用课余时间开发了一款井字棋双人对战游戏,具体内容请点击目录翻页查看~
这个游戏的灵感来源于steam上的一款独立游戏demo - 《tic-tactics》
代码和图片接为本人纯手搓,无 ai 内容{:10_275:}
代码总量约 6000 行左右,其中约 1500 行为 pygameGUI 库的内容
开发时间约三个星期左右
使用 win10 + python 3.12 + pygame 2.6.1 开发
美术素材使用 aseprite 1.3.11 绘制
欢迎二创,代码和美术素材你们都可以使用 (发布时记得@我{:10_259:} )
作者的话: 阿巴阿巴阿巴{:10_330:}
最重要的:求评分
本帖子含有大量图片,如果出现明显图片不显示的情况请刷新重试 (F5)
如果不出意外的话,这应该是目前为止全鱼C完成度最高的 pygame 游戏了,但这也仅仅花费了三个礼拜的时间,代码量6000行{:10_307:}
希望以后我还能再打破这个记录,为各位鱼油们带来更多更好的作品{:10_298:}
具体的未来更新计划看这里:
放假了~ 恢复跟新~
https://fishc.com.cn/thread-251133-1-1.html
(出处: 鱼C论坛)
接下来准备先更新pygameGUI的内容
告诉你们一个秘密:{:10_307:}
**** Hidden Message *****
游戏具体内容
初始界面:
关于界面:
设置界面(调整瓷砖总数):
图鉴界面:
选卡界面:
双方玩家分为红色和蓝色两种阵营,初始/最大血量为 12 :
目前更新的瓷砖(不定期更新瓷砖)
瓷砖:
宝剑:
盾牌:
治疗:
下坠:
陨石:
云朵:
雷电:
火焰:
柠檬:
狙击:
源码/素材及exe程序
由于实在太大了,所以连源码和素材文件都无法上传
所以只能做成百度网盘的形式了,欢迎各位鱼油体验吧~
源码/素材:(有效期一年)
通过网盘分享的文件:tic-tactics源码及素材
链接: https://pan.baidu.com/s/1yVHXOXByhEvIGL8SkRF47Q?pwd=r6nr 提取码: r6nr
exe程序:(有效期一年)
通过网盘分享的文件:tic-tactics源码及素材
链接: https://pan.baidu.com/s/1IA_8fi0P5s3ms9OjzKvxfw?pwd=rjhg 提取码: rjhg
当然,如果觉得网盘太慢的鱼油也可以进群获取~(群精华消息)
源码解读(部分)
由于体量比较大,我只会讲解一些我认为比较有趣的地方, 大家见谅啦~{:10_322:}
main.py 主程序 / 程序框架
本程序分为多个文件
cls文件夹 里面是各种类对象【cls.py】,和瓷砖【tile.py】
image文件夹 里面是所有的素材图片
interface文件夹 各个界面的主循环代码,通过 main.py 调用
pygameGUI文件夹 我自己写的pygame游戏的ui库,这可能是下一期帖子的更新内容,但这个版本不是最终的发布版,可能会有变动 (本帖子不会过多提到关于它的内容)
faction.py 正营的常量(无阵营,蓝方阵营,红方阵营)
func文件夹 定义的一些函数,用于代码复用
最后是两个字体文件,这里不多赘述
main.py 是主程序,负责调配所有代码
首先导入所有的 interface 文件夹中的界面
(关于 interface 内文件的介绍请翻阅目录中 interface文件)
# 导入各个游戏循环(界面
from interface.display_logo import display_logo
from interface.start import start
from interface.elect import elect
from interface.main import main
from interface.end import end
定义窗口,可调整大小,硬件加速,双缓冲
(关于窗口大小调整,请看这个帖子)
pygame窗口:任意缩放
https://fishc.com.cn/thread-245008-1-1.html
(出处: 鱼C论坛)
将窗口数据收集起来,后面通过一个简单的循环调用了 interface文件夹 中的各个界面
# 设定窗口
size = width, height =
window = pygame.display.set_mode(size,RESIZABLE | HWSURFACE | DOUBLEBUF) # 自由拖动大小
screen = pygame.Surface(size).convert_alpha()
pygame.display.set_caption("pygame - tic-tactics - CjjJasonChen")
window_bg = SKY_BLUE
# 时钟
clock = pygame.time.Clock()
# 检测是否全屏
def check_full(full_bool=[],size=[]): # -> None
"""接受一个参数 fullscreen
[窗口现在是否应该处于全屏状态,是否以修正窗口状态]
用于修改窗口状态(大小以及是否全屏,会自动修改fullbool的值)"""
if full_bool and full_bool:
window = pygame.display.set_mode(pygame.display.list_modes(), FULLSCREEN | HWSURFACE | DOUBLEBUF) # 全屏
full_bool = False
elif full_bool:
window = pygame.display.set_mode(size,RESIZABLE | HWSURFACE | DOUBLEBUF) # 自由拖拽窗口
full_bool = False
# 是否全屏
full_bool = # [是否全屏,屏幕的全屏/非全屏是否发生改变]
# 得到鼠标位置的函数
def get_pos(offset=[],ratio=1):
"接受二个参数(屏幕绘制的偏移,屏幕缩放比例)"
pos = pygame.mouse.get_pos()
pos = [(pos-offset)/ratio,
(pos-offset)/ratio]
return pos
# 设置鼠标位置的函数
def set_pos(pos=[],offset=[],ratio=1):
"接受三个参数(鼠标位置,屏幕绘制的偏移,屏幕缩放比例)"
pos = *ratio+offset,
pos*ratio+offset]
pygame.mouse.set_pos(pos)
# 屏幕数据
window_data = {
"clock" : clock, # 时钟
"full_bool" : full_bool, # 判断全屏状态的布尔值列表
"size" : size, # 窗口大小 (原分辨率
"screen" : screen, # 窗口中显示的内容 (Surface对象,大小为size
"window" : window, # 窗口对象
"check_full" : check_full, # 用于改变全屏状态的函数
"bg" : window_bg,
"get_pos" : get_pos,
"set_pos" : set_pos,
}
# logo
window_data, assets = display_logo(window_data,assets)
while 1:
# 开始
window_data, assets = start(window_data,assets)
# 设置
window_data, assets, card_pool1, card_pool2 = elect(window_data, assets)
# 主循环
window_data, assets, winner = main(window_data, assets, card_pool1, card_pool2)
# 结局
window_data, assets = end(window_data, assets, winner)
interface 各个界面
这里用 display_logo.py 作为例子{:10_271:}
导入需要用到库和其他的东西,定义常量
然后是一个很大的函数,这个函数就是会被 main.py 调用的部分
下面是关于函数内容:
解包窗口信息和素材信息 (这个游戏我采用的是每个类自己导入素材的方法,这里的素材信息是预留的,并没有实际作用)
screen = window_data["screen"] # 屏幕surface
window = window_data["window"] # 窗口
check_full = window_data["check_full"] # 检测并修改全屏的函数
full_bool = window_data["full_bool"] # 关于全屏的bool值
size = window_data["size"] # 屏幕大小
clock = window_data["clock"] # 时钟
bg = window_data["bg"] # 背景填充色
# 导入游戏资源
displayed_screen_rect = size # 显示出来的屏幕的矩形位置
ratio = 1 # 屏幕显示时需要缩放的比例
# 关于与鼠标交互的两个函数
set_pos = lambda pos : window_data["set_pos"](pos,
,
displayed_screen_rect],
ratio,)
get_pos = lambda : window_data["get_pos"](,
displayed_screen_rect],
ratio,)
接下来就是实例化粒子特效等内容,这里跳过
最后便是本界面的主循环
running = True
while running:
。。。。
# 刷新界面====
pygame.display.update()
循环结束后,打包好界面信息和素材,通过返回值传给下一个界面程序
window_data["screen"] = screen
window_data["window"] = window
window_data["check_full"] = check_full
window_data["full_bool"] = full_bool
window_data["size"] = size
return window_data,assets
display_logo.py 关于延时计时器和全屏变暗
使用了一个六秒的延时计时器【60帧 * 6秒 = 360帧】
实例化全屏遮罩 (事实上就是一个可调透明的黑色的矩形, 把你的整个屏幕糊住{:10_319:} )
delay = 360 # 延时计时器(6s)
# 遮罩
black_mask = pygame.Surface(,pygame.SRCALPHA)
black_mask.fill(BLACK)
延时计时器每帧减一:
# 延时计时器刷新
if delay == 0:
running = False
else:
delay -= 1
然后看主循环末尾:
# 遮罩
screen.blit(black_mask,)
if delay > 240:
black_mask.set_alpha(255 - (360-delay)/120*255)
elif delay < 120:
black_mask.set_alpha(255 - (delay)/120*255)
程序开始时 至 倒数240 帧遮罩变淡,倒数120帧 至 程序结束 再重新变深
start.py (开始界面 - 粒子从四周飞向鼠标 / 简陋的立体字体)
瓷砖图片列表:
bg_images = []
延时计时器:
delay = 360 # 延时计时器(6s)
...
# 延时计时器刷新
delay -= 1
在屏幕四周生成粒子,飞向鼠标位置
# 生成背景特效
if delay%4 == 0:
side = random.randint(0, 3)
if side == 0:
x, y = random.randint(0, size), size/2 - size/2 - 64
elif side == 1:
x, y = -64, random.randint(int(size/2 - size/2), int(size + size/2 - size/2 +64))
elif side == 2:
x, y = size+64, random.randint(int(size/2 - size/2), int(size + size/2 - size/2 +64))
else:
x, y = random.randint(0, size), size + size/2 - size/2 +64
angle_x = x - pos#sreen.get_rect().width/2
angle_y = y - pos#screen.get_rect().height/2
angle = math.atan2(angle_y, angle_x)
angle = math.degrees(angle)
angle =
pgui.Effect.Burst(
effects,
position=(x, y),
images=bg_images,
angle=angle,
num=1,
speed = random.uniform(3,5),
time=random.randint(60,140),
)
(pgui.Effect.Butst是向任意角度(范围)发射任意数量的粒子,渐变淡,这里用它生成了单个的粒子,且通过math函数计算了发射位置和鼠标位置之间的角度)
关于看起来有立体效果的字体:
假设我需要在 x=100, y=100的位置生成字体 “xxoo”
在 x = 99, y = 99 的位置生成白色或相比“xxoo”亮度更高的相同字体【表示受光面】
在 x = 101, y = 101 的位置生成黑色或相比“xxoo”亮度更低的相同字体 【表示阴影】
大家可以打开 python 尝试一下, 立刻就可以理解了
(正如小甲鱼老师所说,师傅领进门,修行靠个人,这里我只作简单的例子)
elect.py (选择瓷砖界面 - 移动的背景云)
BgCloud:
class BgCloud():
def __init__(self):
bg_cloud1 = pygame.image.load("./image/bg_cloud3.png")
rect = bg_cloud1.get_rect()
bg_cloud1 = pygame.transform.scale(bg_cloud1, (rect.width*4, rect.height*4)).convert_alpha()
bg_cloud3 = pygame.image.load("./image/bg_cloud1.png")
rect = bg_cloud3.get_rect()
bg_cloud3 = pygame.transform.scale(bg_cloud3, (rect.width*4, rect.height*4)).convert_alpha()
self.clouds = [(bg_cloud1, [-200,0]), (bg_cloud3, )]
def update(self):
for i in range(len(self.clouds)):
self.clouds -= (i+1)*2/20
def draw(self, screen):
size = (screen.get_rect().width, screen.get_rect().height)
for cloud in self.clouds:
screen.blit(cloud, (size + cloud, cloud))
screen.blit(cloud, cloud)
注意这里:
self.clouds = [(bg_cloud1, ), (bg_cloud3, )]
记录了两个图片及其 (x,y) 位置
刷新时:
def update(self):
for i in range(len(self.clouds)):
self.clouds -= (i+1)*2/20
(第一个图片每帧移动二十分之一像素,第二个图片移动速度是前一张图片的一倍,也就是十分之一像素)
绘制时:
def draw(self, screen):
size = (screen.get_rect().width, screen.get_rect().height)
for cloud in self.clouds:
screen.blit(cloud, (size + cloud, cloud))
screen.blit(cloud, cloud)
每张图片黏贴了两次,第一次是当前位置,第二次是将图片移出地图的位置补充在空白的位置
由此便实现了云从右侧飞向左侧,近快远慢的背景效果。
main.py (战斗动画)
看到这里的鱼油!恭喜你们一路过关斩将!{:10_275:}
接下可能是这个帖子最有趣的内容了,我将展示我是如何使用 pygame 实现这样比较复杂的战斗动画的
我这里使用的所有动画都是 粒子特效
使用的是我 pygameGUI库 中 widget.py文件 Effect类的内容
主要是粒子对象和粒子管理器对象
class Particle(Widget):
"""粒子"""
...
class Burst(Frame):
...
粒子管理器负责管理粒子。
粒子管理器通过主循环末尾的:
# 刷新xxx=====
guis.update(
events=events,
pos=pos,
mouse=mouse,
plat = plat,
queue = effect_queue,
)
来每帧刷新,一段时间后 (寿命耗尽) 自动删除自己
这时,动画的制作方法就呼之欲出了{:10_275:}
通过函数按照顺序生成粒子管理器。
但是瓷砖连成线,扣除血量等等的判定在 “ok” 按钮按下的那一帧就已经完成了,要如何一个一个的展示动画呢?
注意看这里:
effect_delay = # 特效计时器,放映特效时通过它申请时间
effect_queue = [] #
上面的“a”就是指生成动画的函数,
生成动画时将生成动画的函数打包为 lambda函数,后面跟一个播放动画需要的时间,将其加入动画播放队列。
if effect_delay >0:
effect_delay -= 1
elif effect_queue:
e = effect_queue.pop(0)
e()
effect_delay += e
每帧自动处理动画队列中的内容,这样就实现了按照顺序一个一个播放动画。
(事实上,我将部分判定也做成函数塞到了动画播放序列内,与播放动画的同时进行判定)
下面举一个例子:
effect_delay = # 特效计时器,放映特效时通过它申请时间
effect_queue = [] #
def display_effect(text):
print(text)
fps = 0
while 1:
print(fps)
fps += 1
if effect_delay >0:
effect_delay -= 1
elif effect_queue:
e = effect_queue.pop(0)
e()
effect_delay += e
if input("是否要生成动画(y/n):") == "y":
text = input("请输入要显示的文本:")
delay = int(input("请输入要申请的帧数:"))
effect_queue.append(
[
lambda text=text:
display_effect(text),
delay
]
)
运行效果:
大家学会了吗{:10_275:}
百度网盘下载实在一言难尽……{:10_277:}
分析了一下各个文件的大小,剔除字体文件后成功实现了代码及素材的上传。
需要的字体:
STXINGKA.TTF
ARIALUNI.TTF
将字体文件下载好,移入main.py所在目录即可 @小甲鱼 @中英文泡椒 @学习编程中的Ben @python爱好者. @zhangjinxuan @liuhongrun2022 @陶远航 @琅琊王朝 @不二如是 @过默 @Ewan-Ahiouy @zhangchenyvn @某一个“天” @zhangjinxuan @sfqxx @KeyError @世味 @赵廷敬 @魔女库伊拉 @1909997160
快来看我的帖子! {:10_257:} 厉害{:10_275:} 厉害啦{:10_275:} 中英文泡椒 发表于 2025-7-21 15:21
厉害啦
{:10_278:}只要主题评分就可以了,宣传贴不用评分的 期待更多作品! cjjJasonchen 发表于 2025-7-21 15:26
只要主题评分就可以了,宣传贴不用评分的
{:10_275:} 建议申精{:10_254:} 杨哲予 发表于 2025-7-21 15:53
建议申精
要有足够的评分才可以申请精华 支持 赞 smallwh 发表于 2025-7-21 17:54
百度网盘下载实在一言难尽……
分析了一下各个文件的大小,剔除字体文件后成功实现了代码及素 ...
字体文件应该windows的fonts里面有,我用的都是我电脑自带的字体 捉虫:
第九页 在 下= 101
应该是x{:10_256:}{:10_256:} 好赞,相当细腻的开发 {:13_445:} 支持{:13_444:} 某一个“天” 发表于 2025-7-21 18:03
捉虫:
第九页 在 下= 101
应该是x
{:10_250:}收到 匿名鱼油 发表于 2025-7-21 18:17
支持
又是你!显身卡无效同志{:10_282:} cjjJasonchen 发表于 2025-7-21 18:52
又是你!显身卡无效同志
没错又是我{:10_256:}我现在的等级还太低了,没有加好友的权限{:10_266:}