消除麻将
根据游戏规则,两张相同图案的麻将,如果互相之间没有其他麻将牌被直线阻隔(中间的距离可以无限),可以通过先后点击选择这两张麻将,消除这两张牌。
要实现以上功能,需要分步完成以下几个能力:
- 要能实现“先后选中”的能力,因此要对鼠标点击的操作做出响应。
- 需要能控制显示、消失图像,用以表现“选中”麻将,以及显示“消除”的效果。
- 通过“桌子”的内置数据结构,对麻将牌的位置是否成直线、两个选中的麻将判断是否有阻隔。
选中麻将
对于麻将类 Mahjong 的 update() 方法,增加对于用户输入事件的检测和处理,就能完成“选中麻将”的功能:
第一篇介绍的 Director 类,会在每一帧,都通过 pygame 把所有的用户输入事件,存放到 Director.events 属性中,所以每个 Sprite 的子类对象,都可以在 update() 函数中去检测判断:用户有什么输入。
由于 mahjong.MainScenario 类,在 start() 方法中,构建 Table 这个 Group 的子类对象时,传入了 director 参数
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 就是麻将对象,所以我们就判断鼠标是否“点击”到了当前的麻将。
显示选中特效
对于选中的麻将,我们希望是:
- 如果第一次选中麻将,在被选中的麻将上显示一个“框框”
- 被选中的麻将,需要以某个方式记录其坐标
- 如果已经有一个麻将被选中,选中第二个麻将后,“框框”消失
所有需要控制显示的对象,都继承 Sprite 实现一个类,通过构造器来实现加载某个图像数据。此对象的 image/rect 属性通过加载一个图片作为框框显示,这个图片需要是中间透明的,所以使用的 png 格式。
我们可以建立一个类 Edge,用来显示“选中框”。此类有的 pos 属性是一个数组,记录选中的麻将牌的桌上坐标。
Table 类添加一个属性 edge,持有此 Edge 对象;另外一个属性 is_show_edge 记录框框是否已经显示。
在每帧都调用的 Table.show() 方法里面,根据 is_show_edge 属性,来决定是否 add(self.edge),就可以实现根据 is_show_edge 来显示/消失这个框框。
由于每次显示 Edge 对象的位置不一样,所以在 Table 上增加了一个 show_edge() 方法,用来修改 Table.edge 的位置:
其中 loc 参数表示被选中麻将的坐标,会记录到 Edge.pos 上,同时根据此坐标计算并修改 edge.rect 的位置,并且对 is_show_edge 赋值为 True;当点击事件触发“点击第二张牌”的时候,此属性会被置为 False。
选中第二个牌的处理
点击第二张牌后,需要判断是否可以消除,代码在 Mahjong.update():
由于 Table.show_edge() 会在 table.edge.pos 记录被选中的第一张麻将的坐标,所以第二张麻将被选中的时候,可以通过这个坐标(i,j)从 table.heap 这个二维数组获得被选中的麻将。
下面就是几个情况,判断是否可以消除,具体判断:
- 两个牌直接是否有阻隔
- 被选中的牌不能是空
- 两张牌的图案是一样的
- 不能选中两次是同一张牌
如果可以消除,通过对 heap[x][y] 的值赋值 None 就表示了消除。在 Table.show() 里面,会跳过为 None 的 heap 成员,因此就可以作为消除牌的功能实现。
如果不能消除,这里调用了一个 Table.show_text() 方法,用于显示提示文字,后续会介绍如何显示。
显示爆炸效果
在上述逻辑中,通过了以下代码实现“显示”爆炸效果:
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 对象,然后在需要爆炸的时候,再显示到对应的位置。
具体的方法是:
- 修改自己的显示位置,把自己 add 到“特效层”的 effect 组里
- 设置一个倒计时属性 counter,需要显示多少帧时间,就设置为多少,这里是 30,也就是一秒,因为 director.fps 设置了 30
- 通过 update() 方法,每帧对 counter 减一,如果为 0,则从 effect 组里去掉(通过 Group.remove(Sprite) 方法),从而消失。由于 effect 组并不会每帧都清空所有成员,和 table 组不一样,所以不需要每次 update() 都去 add() 一次自己
显示文字提示
文字提示,实际上也是一种 Sprite 对象,也需要对 image/rect 进行赋值,和上面的图像不同的是,文字的 image 需要通过选择字体和文字内容进行绘制。如果要显示一段文字在游戏画面上,只需要:
从上面的代码可以看出,我们可以选择文字的字体、颜色,还可以选择和其他内容共同“画”在一个图形上。
由于本游戏只需要在一个地方显示文字,而且字体只需要一种,所以在 Table 对象的属性中构造好字体对象 font、显示文字对象这两个对象 text_sprite。另外,这个提示文字需要自动消失,所以还需要两个属性来记录文字显示了几秒 show_text_time,以及何时开始 start_ticks。这个自动消失的功能和上面的爆炸特效功能类似,但是这里使用了不同方法,纯粹为了学习。
然后写一个 show_text() 的方法,用来在桌上显示文字:
这里需要注意的是 self.show_text_time = time 这句,是记录了当前文字要显示多少秒,这个值会在 update() 中逐渐减少,用以让文字自动消失。下面是 Table.show() 的代码段:
这里可以看到,每次 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() 特别慢,整个游戏的运行就会感觉特别卡。
下一篇介绍如何实现麻将的移动动画,以及复杂的游戏逻辑判断。