前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用 PyGame 入门专业游戏开发(三)

用 PyGame 入门专业游戏开发(三)

作者头像
韩伟
发布2023-12-12 17:20:02
1810
发布2023-12-12 17:20:02
举报
文章被收录于专栏:韩伟的专栏

消除麻将

根据游戏规则,两张相同图案的麻将,如果互相之间没有其他麻将牌被直线阻隔(中间的距离可以无限),可以通过先后点击选择这两张麻将,消除这两张牌。

要实现以上功能,需要分步完成以下几个能力:

  1. 要能实现“先后选中”的能力,因此要对鼠标点击的操作做出响应。
  2. 需要能控制显示、消失图像,用以表现“选中”麻将,以及显示“消除”的效果。
  3. 通过“桌子”的内置数据结构,对麻将牌的位置是否成直线、两个选中的麻将判断是否有阻隔。

选中麻将

对于麻将类 Mahjong 的 update() 方法,增加对于用户输入事件的检测和处理,就能完成“选中麻将”的功能:

代码语言:javascript
复制
    def update(self):
        if self.table == None or self.table.director == None:
            return

        for event in self.table.director.events:
            if event.type != pygame.MOUSEBUTTONDOWN:
                continue

            # 检测精灵是否被点击
            mouse_pos = pygame.mouse.get_pos()
            if self.rect.collidepoint(mouse_pos) == False:
                return

            if self.table.is_show_edge == False:
                self.table.show_edge(self.pos) # 选中第一张牌
            else:
                # 选中第二张牌
                ......

第一篇介绍的 Director 类,会在每一帧,都通过 pygame 把所有的用户输入事件,存放到 Director.events 属性中,所以每个 Sprite 的子类对象,都可以在 update() 函数中去检测判断:用户有什么输入。

由于 mahjong.MainScenario 类,在 start() 方法中,构建 Table 这个 Group 的子类对象时,传入了 director 参数

代码语言:javascript
复制
table = Table(self.director)

因此每个 Mahjong 对象,都可以通过其 table 属性获得 director 对象,从而获得每帧更新的用户事件(们):self.table.director.events

桌上所有的 Mahjong 对象,由于存放在 Table 这个 Group 里面,所以每帧其 update() 都会被调用。也就是说,每帧、每个麻将对象,都可以在 update() 里检测一遍:“我”有没有被鼠标点中。用户在此刻的所有操作,会被 pygame 放入 events 列表,需要我们通过循环迭代语句,获取其中的每个事件。

通过 event.type 属性,判断 pygame.MOUSEBUTTONDOWN 就可以知道是否有鼠标按钮按下的事件;随后可以通过 pygame.mouse.get_pos() 可以获得鼠标当前的位置;最后通过 Sprite.rect.collidepoint(pos) 可以判断当前 Sprite 对象是否有“碰撞”到某个 pos 点位置。当前的 Sprite 就是麻将对象,所以我们就判断鼠标是否“点击”到了当前的麻将。

显示选中特效

对于选中的麻将,我们希望是:

  1. 如果第一次选中麻将,在被选中的麻将上显示一个“框框”
  2. 被选中的麻将,需要以某个方式记录其坐标
  3. 如果已经有一个麻将被选中,选中第二个麻将后,“框框”消失

所有需要控制显示的对象,都继承 Sprite 实现一个类,通过构造器来实现加载某个图像数据。此对象的 image/rect 属性通过加载一个图片作为框框显示,这个图片需要是中间透明的,所以使用的 png 格式。

我们可以建立一个类 Edge,用来显示“选中框”。此类有的 pos 属性是一个数组,记录选中的麻将牌的桌上坐标。

代码语言:javascript
复制
class Edge(pygame.sprite.Sprite):
    '''提示的点'''

    def __init__(self, table):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load("edge.png")
        self.rect = self.image.get_rect()
        self.table = table  # 所在桌子
        self.pos = [0,0] # 标记框框套在哪个麻将上面,记录在桌上的坐标

    def update(self):
        pass

Table 类添加一个属性 edge,持有此 Edge 对象;另外一个属性 is_show_edge 记录框框是否已经显示。

在每帧都调用的 Table.show() 方法里面,根据 is_show_edge 属性,来决定是否 add(self.edge),就可以实现根据 is_show_edge 来显示/消失这个框框。

由于每次显示 Edge 对象的位置不一样,所以在 Table 上增加了一个 show_edge() 方法,用来修改 Table.edge 的位置:

代码语言:javascript
复制
    def show_edge(self, loc: list[int]):
        self.edge.rect.left = self.edge.rect.width*loc[0]
        self.edge.rect.top = self.edge.rect.height*loc[1]
        self.edge.pos = loc
        self.is_show_edge = True

其中 loc 参数表示被选中麻将的坐标,会记录到 Edge.pos 上,同时根据此坐标计算并修改 edge.rect 的位置,并且对 is_show_edge 赋值为 True;当点击事件触发“点击第二张牌”的时候,此属性会被置为 False。

选中第二个牌的处理

点击第二张牌后,需要判断是否可以消除,代码在 Mahjong.update():

代码语言:javascript
复制
def update(self):
       # 判断事件和选中第一张牌
       ......
            else:
                # 选中第二张牌
                i = self.table.edge.pos[0] # 选中的第一张牌的 X 坐标
                j = self.table.edge.pos[1] # 选中的第一张牌的 Y 坐标
                selected = self.table.heap[i][j] # 获得选中的第一张牌对象

                x = self.pos[0]
                y = self.pos[1]
                # 判断:两个牌直接是否有阻隔 and 被选中的牌不能是空 and 两张牌的图案是一样的 and 不能选中两次是同一张牌
                if self.table.can_do([i,j],[x,y]) and selected != None and selected.symbol == self.symbol and self.table.heap[i][j] != self.table.heap[x][y]:
                    # 消除两张牌
                    self.table.heap[i][j] = None
                    self.table.heap[x][y] = None
                    #显示特效
                    self.bomb.show(self.rect.left,self.rect.top)
                    selected.bomb.show(selected.rect.left,selected.rect.top)
                else:
                    self.table.show_text('直线相连的两张同样的牌才能消除!')
                self.table.hide_edge()

由于 Table.show_edge() 会在 table.edge.pos 记录被选中的第一张麻将的坐标,所以第二张麻将被选中的时候,可以通过这个坐标(i,j)从 table.heap 这个二维数组获得被选中的麻将。

下面就是几个情况,判断是否可以消除,具体判断:

  • 两个牌直接是否有阻隔
  • 被选中的牌不能是空
  • 两张牌的图案是一样的
  • 不能选中两次是同一张牌

如果可以消除,通过对 heap[x][y] 的值赋值 None 就表示了消除。在 Table.show() 里面,会跳过为 None 的 heap 成员,因此就可以作为消除牌的功能实现。

如果不能消除,这里调用了一个 Table.show_text() 方法,用于显示提示文字,后续会介绍如何显示。

显示爆炸效果

在上述逻辑中,通过了以下代码实现“显示”爆炸效果:

代码语言:javascript
复制
self.bomb.show(self.rect.left,self.rect.top)
selected.bomb.show(selected.rect.left,selected.rect.top)

由于在 MainSenario 的 start() 方法中,为每个麻将对象,都添加了一个爆炸对象属性 Mahjong.bomb,所以被选中的两个麻将对象,都可以调用 self.bomb.show() 这个方法,传入了需要显示的坐标。一旦调用这个方法,Bomb 类就会自己通过 Bomb.update() 方法,显示一段时间“爆炸”的图片。如果想内存占用的小一点,也可以在 MainSenario.start() 方法中只构造两个 Bomb 对象,然后在需要爆炸的时候,再显示到对应的位置。

具体的方法是:

  1. 修改自己的显示位置,把自己 add 到“特效层”的 effect 组里
  2. 设置一个倒计时属性 counter,需要显示多少帧时间,就设置为多少,这里是 30,也就是一秒,因为 director.fps 设置了 30
  3. 通过 update() 方法,每帧对 counter 减一,如果为 0,则从 effect 组里去掉(通过 Group.remove(Sprite) 方法),从而消失。由于 effect 组并不会每帧都清空所有成员,和 table 组不一样,所以不需要每次 update() 都去 add() 一次自己
代码语言:javascript
复制
class Bomb(pygame.sprite.Sprite):
    '''消除特效'''

    def __init__(self, effect:pygame.sprite.Group):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load("Bomb_effect.png")
        self.effect = effect
        self.counter = 0
        self.rect = self.image.get_rect()

    def show(self,left,top):
        self.effect.add(self)
        self.counter = 30
        self.rect.left = left
        self.rect.top = top

    def update(self):
        self.counter -= 1 # 倒计时减一
        if self.counter == 0: # 结束显示特效
            self.effect.remove(self)

显示文字提示

文字提示,实际上也是一种 Sprite 对象,也需要对 image/rect 进行赋值,和上面的图像不同的是,文字的 image 需要通过选择字体和文字内容进行绘制。如果要显示一段文字在游戏画面上,只需要:

代码语言:javascript
复制
# 获得字体 SimSun 32 号
font = pygame.font.SysFont("SimSun", 32)

# 用font字体渲染内容为 hello world 的橙色文字 
text = font.render("hello world", True, pygame.Color("orange"))

# 获得渲染后的文字(图像)大小
rect = self.text.get_rect()

# 生成一个刚好是上面文字图像大小的 image 对象
image = pygame.Surface((rect.width, rect.height))

# 把文字绘制在 image 对象的 0,0 位置
image.blit(text, (0,0))

从上面的代码可以看出,我们可以选择文字的字体、颜色,还可以选择和其他内容共同“画”在一个图形上。

由于本游戏只需要在一个地方显示文字,而且字体只需要一种,所以在 Table 对象的属性中构造好字体对象 font、显示文字对象这两个对象 text_sprite。另外,这个提示文字需要自动消失,所以还需要两个属性来记录文字显示了几秒 show_text_time,以及何时开始 start_ticks。这个自动消失的功能和上面的爆炸特效功能类似,但是这里使用了不同方法,纯粹为了学习。

代码语言:javascript
复制
class Table(pygame.sprite.Group):
    '''控制生成的桌面'''

    def __init__(self, director: scenario.Director):
        .......
        # 构建提示语句
        self.font = pygame.font.SysFont("SimSun", 32)
        self.text_sprite = pygame.sprite.Sprite()
        self.text = ""
        self.show_text_time = 0 # 文字需要显示几秒
        self.start_ticks = 0 # 文字开始显示的时间

然后写一个 show_text() 的方法,用来在桌上显示文字:

代码语言:javascript
复制
def show_text(self, content:str, time:int = 2):
        '''显示提示文字在屏幕上面 time 秒'''
        self.text = self.font.render(content, True, pygame.Color("orange"))
        self.show_text_time = time
        self.start_ticks = pygame.time.get_ticks() #starter tick
        rect = self.text.get_rect()
        self.text_sprite.rect = rect
        self.text_sprite.image = pygame.Surface((rect.width, rect.height))
        self.text_sprite.image.blit(self.text, (0,0))
        print("show text:", content)
        pass

这里需要注意的是 self.show_text_time = time 这句,是记录了当前文字要显示多少秒,这个值会在 update() 中逐渐减少,用以让文字自动消失。下面是 Table.show() 的代码段:

代码语言:javascript
复制
def show(self):
        self.empty()

        # 画麻将牌
        ......

        # 画框框
        ......

        # 画文字
        if self.show_text_time > 0:
            seconds = (pygame.time.get_ticks()-self.start_ticks)/1000 #calculate how many seconds
            if seconds<self.show_text_time: # if more than 10 seconds close the game
                self.add(self.text_sprite)
            else:
                self.remove(self.text)
                self.show_text_time = 0
        pass

这里可以看到,每次 update() 调用 show(),然后都会判断一下 show_text_time,用以决定是否要显示文字提示。由于 self.start_ticks 记录了启动显示的时间,所以根据 pygame.time.get_ticks() 返回的当前时间(毫秒数),就能知道已经显示了多久。显示和消失也是用 add() 和 remove() 控制。由于 Table.show() 的第一行是 self.empty(),会清空所有在 table 这个 Group 里的 Sprite,所以下面要显示的内容,都必须要调用 self.add()。

从上面的代码可以看到,游戏程序的所有“动态能力”,基本实现思想都是:

  • 每个游戏对象在构造器或者初始化函数中,构建好所需的各种对象
  • 通过每帧调用 update() 函数进行“驱动”
  • 在每帧的时刻,进行用户操作检测
  • 在每帧的时刻,计算出当前帧游戏的内部逻辑的状态
  • 根据当前帧的状态,控制在屏幕上合适的位置,实现显示、消失

因此,游戏系统的动画,也大多数是如此实现,是通过一帧帧的逻辑,来决定如何显示下一个画面,从而形成一个动画。由于 udpate() 函数每帧都要调用,所以尽量减少在这个函数中构建新的对象,或者进行特别慢的操作如等待加载磁盘文件、等待网络响应等。因为如果 update() 特别慢,整个游戏的运行就会感觉特别卡。

下一篇介绍如何实现麻将的移动动画,以及复杂的游戏逻辑判断。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-12-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 韩大 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档