cjjJasonchen 发表于 5 天前

【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:}


smallwh 发表于 5 天前

百度网盘下载实在一言难尽……{:10_277:}
分析了一下各个文件的大小,剔除字体文件后成功实现了代码及素材的上传。

需要的字体:
STXINGKA.TTF
ARIALUNI.TTF
将字体文件下载好,移入main.py所在目录即可

cjjJasonchen 发表于 5 天前

@小甲鱼 @中英文泡椒 @学习编程中的Ben @python爱好者. @zhangjinxuan @liuhongrun2022 @陶远航 @琅琊王朝 @不二如是 @过默 @Ewan-Ahiouy @zhangchenyvn @某一个“天” @zhangjinxuan @sfqxx @KeyError @世味 @赵廷敬 @魔女库伊拉 @1909997160

快来看我的帖子!

快速收敛 发表于 5 天前

{:10_257:}

王昊扬 发表于 5 天前

厉害{:10_275:}

中英文泡椒 发表于 5 天前

厉害啦{:10_275:}

cjjJasonchen 发表于 5 天前

中英文泡椒 发表于 2025-7-21 15:21
厉害啦

{:10_278:}只要主题评分就可以了,宣传贴不用评分的

不二如是 发表于 5 天前

期待更多作品!

某一个“天” 发表于 5 天前

cjjJasonchen 发表于 2025-7-21 15:26
只要主题评分就可以了,宣传贴不用评分的

{:10_275:}

杨哲予 发表于 5 天前

建议申精{:10_254:}

cjjJasonchen 发表于 5 天前

杨哲予 发表于 2025-7-21 15:53
建议申精

要有足够的评分才可以申请精华

Mike_python小 发表于 5 天前

支持

intp-睡教教主 发表于 5 天前

cjjJasonchen 发表于 5 天前

smallwh 发表于 2025-7-21 17:54
百度网盘下载实在一言难尽……
分析了一下各个文件的大小,剔除字体文件后成功实现了代码及素 ...

字体文件应该windows的fonts里面有,我用的都是我电脑自带的字体

某一个“天” 发表于 5 天前

捉虫:
第九页   在 下= 101
应该是x{:10_256:}{:10_256:}

小甲鱼 发表于 5 天前

好赞,相当细腻的开发 {:13_445:}

匿名鱼油 发表于 5 天前

支持{:13_444:}

cjjJasonchen 发表于 5 天前

某一个“天” 发表于 2025-7-21 18:03
捉虫:
第九页   在 下= 101
应该是x

{:10_250:}收到

cjjJasonchen 发表于 5 天前

匿名鱼油 发表于 2025-7-21 18:17
支持

又是你!显身卡无效同志{:10_282:}

匿名鱼油 发表于 5 天前

cjjJasonchen 发表于 2025-7-21 18:52
又是你!显身卡无效同志

没错又是我{:10_256:}我现在的等级还太低了,没有加好友的权限{:10_266:}
页: [1] 2 3 4 5
查看完整版本: 【Python + pygame】独立游戏 - 【战术井字棋 (tic-tactics)】(鱼币)