推麻将的玩法
上一篇介绍了一个游戏运行的最基本结构,本篇开始根据一个具体的游戏,做一个游戏关卡。下面要做的是一个叫“推麻将”的桌面玩法。现在介绍一下这个玩法的具体内容:
- 一副麻将随机放在桌上,共 8 行 14 列
- 任何两个相同的麻将,直线相连如果没有其他麻将阻隔,就可以消除掉
- 桌上如果有空位(有麻将消除了留下的空位),相邻的四个方向的麻将行列,都可以整队移动;但是移动之后,被推动的这队麻将,必须至少要有一个能被消除的麻将,否则不能移动
- 桌上所有麻将都被消除完就是胜利;
- 消除和推动麻将的移动,使用鼠标点击来操作
第一个关卡
根据上篇设计的关卡基类 Scenario,我们可以为了一个特定的游戏,建立一个子类 MainScenario。
编写 MainScenario 也很简单,主要就是实现一个 start() 方法。此方法所需要做的事情,就是多次调用基类的 add_group() 方法,把需要显示的游戏对象,都以 Group 的组织形式,添加到关卡中去。
Group 对象及其内部的 Sprite 对象,一旦被 add_group() 放到 MainScenario 后,由于 Director 的 run() 方法,就会每帧(每秒60次的)去调用 MainScenario 的 update() 方法,因此在 MainScenario 中的 Group 对象,以及 Sprite 对象的 update() 方法也会被调用。所以我们游戏逻辑的主要实现代码就是:
- 编写 MainScenario.start() :放置游戏关卡初始的所有游戏对象组 Group 以及需要的游戏对象 Sprite
- 编写游戏对象 Group 和 Sprite 的子类,通过实现 __init__() 和 update() 方法来完成各种游戏行为
class MainScenario(scenario.Scenario):
'''入口的局'''
def __init__(self):
scenario.Scenario.__init__(self)
def start(self):
'''剧幕开场'''
# 建立各图层
table = Table(self.director) # 桌子
bg = pygame.sprite.Group() # 背景
effect = pygame.sprite.Group() # 特效
# 各图层放上舞台
self.add_group("bg", bg)
self.add_group("table", table)
self.add_group("effect",effect)
…………………………
上面的代码,在关卡中加入了三个 Group:
- bg 代表背景,在上面的游戏中,是由一批带圆点花纹的 Sprite 组成的桌布
- table 代表桌子,上面这个游戏是一个放了几十个麻将牌的桌子,其中每个麻将是一个 Sprite,桌子 Table 类则继承 Group
- effect 代表特效层,特效层初始化的时候,没有任何的 Sprite 成员,而是在运行时添加和删除“爆炸特效” Sprite,用来显示“消除”麻将的效果。因此建立了 bomb1/bomb2 两个 Sprite 对象,先作为属性挂在每个麻将 Sprite 对象上。而 bomb1/bomb2 对象的类 Bomb,也会保存 effect 这个 Group 的对象,用以实现动画效果。
注意三个 Group 的 add_group() 的顺序:最先添加的,会被放在最底层显示,以此类推。所以 bg 作为背景是最底下,中间是 table 层,上面是特效 effect 层。三个 Group 对象通过 add_group() 放入到关卡 MainSenario 中。
然后根据游戏玩法我们设计了几个类,用来实现上述的玩法:
- Table:存放所有麻将的对象,会记录所有麻将的位置,每帧根据麻将的位置重绘画面,麻将移动过程也是通过 Table 显示。
- Mahjong:可以放在 Table 上显示,一个关卡中会有 8x14 共 112 个对象,每个对象保存自己的图案。点击麻将的事件处理也由此类处理。
- Edge:点击麻将后,显示的“选中”框,通过 effect 这个 Group 显示。Table 对象会记录 Edge 的位置,以记录当前选定的麻将。
- Point:桌面背景层,通过 bg 这个 Group 显示。点击 Point 会触发麻将的移动逻辑。桌面上也是由 112 个 Point 对象组成,因此被点击的 Point 是可以知道其坐标位置的。
- Bomb:消除麻将时显示的“爆炸”动画,每个麻将对象身上都有属性是 Bomb 对象(b1/b2),需要显示的时候直接加入 effect Group,过一段时间后消失,形成一个简单的动画效果。
最终,上面所有的 Sprite,都以所需的游戏逻辑构建,并且被放入 Group 中。
代码语言:javascript复制
def start(self):
'''剧幕开场'''
# 建立各图层
table = Table(self.director) # 桌子
bg = pygame.sprite.Group() # 背景
effect = pygame.sprite.Group() # 特效
# 各图层放上舞台
self.add_group("bg", bg)
self.add_group("table", table)
self.add_group("effect",effect)
# 画桌布背景
for i in range(0, Table.cols):
for j in range(0, Table.rows):
point = Point(table)
point.rect.left = point.rect.width*i
point.rect.top = point.rect.height*j
point.pos = [i, j]
bg.add(point)
# 生成 112 张牌
heap = []
for j in range(0, Mahjong.cols):
for i in range(0, Mahjong.lines):
for k in range(0, 4): # 每个图案生成 4 张麻将
if i != 3 or j == 5: # 第 4 行素材只取红中
dot_one = Mahjong(table, [j, i])
heap.append(dot_one)
table.put_in(heap)
对于游戏来说,为每个可以单独显示的“东西”设计一个类,是非常自然的做法;然而,有一些并不可见的逻辑,也应该考虑设计成一个类,譬如这里的 Table 类型。事实上,Table 对象保存了整个游戏程序中最重要的状态,就是所有麻将的位置。有了 Table 对象,其他所有的可显示对象,在处理“被鼠标点击”事件的时候,都能获得完整的所有麻将的状态,非常方便编写游戏业务逻辑。
加载图像资源
在处理完“桌子”之后,下来需要处理的最复杂的资源,就是麻将了。一般来说,游戏的图像资源,都是一个图片文件。很多图像都拼接在同一个文件上,如下图:
每个麻将需要获得这个文件中图像的某一块,需要有两个步骤:
- 把整个图片加载到内存中,变成一个对象(变量)
- 截取自己需要的那一部分图像,变成一个对象,存放到 Mahjong 对象的 image 属性和 Rect 属性上。image 属性是 Sprite 基类规定了,用来显示的图像内容属性。而 Rect 属性则决定此 Sprite 对象显示在屏幕上的位置和大小。
class Mahjong(pygame.sprite.Sprite):
'''麻将'''
# 完整素材图包含了 9X4 的子图片,每个牌希望尺寸为 45x62
bigImage = pygame.image.load("southeast.jpg")
cols = 9
lines = 4
margin_width = 16
margin_height = 8
moving_speed = 5
def __init__(self, table, symbol=[0, 0]):
pygame.sprite.Sprite.__init__(self)
self.symbol = symbol # 麻将符号
self.table = table
self.pos = [0, 0] # 在桌上的位置
self.is_moving = False
effect = table.director.current_scenario.stage_map["effect"] # 通过“名字”获取特效层
self.bomb = Bomb(effect) # 爆炸特效
# 从素材图中获取尺寸
unitWidth = Mahjong.bigImage.get_rect().width/Mahjong.cols
unitHeight = Mahjong.bigImage.get_rect().height/Mahjong.lines
self.image = pygame.Surface(
(unitWidth-2*Mahjong.margin_width, unitHeight-2*Mahjong.margin_height))
self.rect = self.image.get_rect()
# 选择具体牌,symbol 为第几行第几列的牌
self.image.blit(Mahjong.bigImage, (0, 0),
(symbol[0]*unitWidth Mahjong.margin_width, symbol[1]*unitHeight Mahjong.margin_height, self.rect.width, self.rect.height))
上述代码的 pygame.image.load() 是作为类静态代码执行,只会执行一次,并不会每个 Mahjong 对象构造出来都运行一次。这行代码就是加载图片资源:一个由 36 个麻将组成的图片。
上述代码的 self.image.blit() 就是从一个 pygame.surface.Surface 对象上,截取某一块图像作为内容。至于需要截取哪一块图像,由 symbol 参数决定,这个参数以一个二位数组,标识一个麻将花色。这个数值的内容,也代表了在图形文件 southeast.jpg 上具体某一行、列的麻将图像。从此,Mahjong 对象有了可以显示的内容,只要把此对象 add() 到一个 Group 上,屏幕就会显示一个麻将牌了。
通过 symbol 的数值,可以计算出 southeast.jpg 图像文件上具体的图像的位置。并且通过设定的空白边的高、宽,准确截取想要的图像。以上的加载图像代码,包含了 cols/lines/margin_width/magin_height 这些常量,这些数值是和 southeast.jpg 绑定的。在 Unity 等游戏引擎中,通常会有一些图形文件处理工具,来帮你以可视化的方式,切割一整个图形文件,然后生成你需要的各个游戏对象(Sprite)。
随机生成一桌麻将
代码语言:javascript复制 # 生成 112 张牌
heap = []
for j in range(0, Mahjong.cols):
for i in range(0, Mahjong.lines):
for k in range(0, 4): # 每个图案生成 4 张麻将
if i != 3 or j == 5: # 第 4 行素材只取红中
dot_one = Mahjong(table, [j, i])
heap.append(dot_one)
table.put_in(heap)
上述代码在 MainScenario.start() 中,对于 9x4 的图形资源,每取出一个,就生成 4 个相同的 Mahjong 对象。循环中的 [j, i] 变量,代表了麻将的图案。然后把这 112 个麻将放在一个数组中,通过 Table.put_in() 放到桌上。
一般来说,麻将的图案和麻将美术资源应该是解耦的,上面代码中的 Mahjong.cols, Mahjong.lines 这两个常量,决定了生成的 Mahjong 对象的 symbol 属性的值,如 [0,1] 代表“二筒”、[1,2] 代表“三条”。按专业的做法,这个值(如 [0,1],[1,2])是不应该是根据 southeast.jpg 这个图片上对应图案的“坐标”来确定的,而应该有另外一个配置文件,写下每个麻将图案代表的数值(可能是从 0-36),对应美术资源 southeast.jpg 文件上的位置坐标。但是这个游戏比较简单,麻将的图形文件也不太可能更换,所以代码中这么写也可以接受。因此 Mahjong.symbol 属性就是由两个 int 组成的数组。这样使用美术资源的图像坐标,代表麻将图案,由于是一个两个元素的数组变量,让代码的理解也变得困难了一些。
Table 对象通过一个属性 heap 记录每个麻将的位置,heap 是一个 14x8 的二维数组,下标是桌上麻将的行、列数字,元素则是 Mahjong 对象。如果某个位置没有麻将,这个坐标所对应的值是 None。 由于需要随机打乱位置,所以 Table.put_in() 必须要使用随机数来实现这个功能:
- 用一个数组 mahjiongs 存放“未放入”的麻将堆
- 用一个数组 random_symbol 存放“打乱顺序”的麻将堆
- 随机从 mahjiongs 抽出一个麻将,加入到 random_symbol 中,直到 mahjiongs 变空
- 用 random_symbol 的顺序,一个个放入 Table 的 14x8 的数组 heap 中。
def put_in(self, maijiangs: list[Mahjong]):
'''打乱放入桌子'''
random_symbol = []
while len(maijiangs) != 0:
index = random.randint(0, len(maijiangs))
a = maijiangs.pop(index-1)
random_symbol.append(a)
index = 0
for i in range(0, Table.cols):
for j in range(0, Table.rows):
theMajiang = random_symbol[index] # 取出打乱队列中的麻将
index = index 1
self.heap[i][j] = theMajiang
theMajiang.pos = [i, j] # 让麻将知道自己的坐标
Table 通过 heap 属性,记录所有的麻将,然后通过对 Majiong.pos 赋值,传入其所在 heap 数组的坐标,让每个 Mahjong 自己调整 Rect 属性,从而实现按预定桌面位置进行显示:
代码语言:javascript复制 def show(self):
self.empty() # 清空 Group 里所有 Sprite,以便下面重新画
# 画麻将牌
for i in range(0, Table.cols):
for j in range(0, Table.rows):
theMajiang = self.heap[i][j]
if theMajiang == None:
continue
theMajiang.show() #根据位置调整自己的Rect
self.add(theMajiang) # Sprite加入到Group中显示
以上的 Table.show() 方法,会在 Table.update() 中调用,索引每帧都会刷新显示桌面上所有麻将的位置。这样游戏逻辑,只需要修改 Table.heap 的内容,就能自由控制桌面上需要显示的麻将了。
上面的 theMajiang.show(),实际上是根据 Mahjong.pos 属性去设置自己的 Rect 数值,以确定显示位置的。而 Mahjong.pos 属性,在 Table.put_in() 的时候已经正确赋值了。
代码语言:javascript复制 def show(self):
'''显示麻将牌'''
if self.is_moving == False:
self.rect.left = self.rect.width*self.pos[0]
self.rect.top = self.rect.height*self.pos[1]
return
Mahjong.show() 还有一个功能,就是显示麻将牌移动的动画效果,这个在下一篇再讲。后续会附带上完整的代码 mahjiong.py。