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

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

作者头像
韩伟
发布2023-12-04 12:48:13
1910
发布2023-12-04 12:48:13
举报
文章被收录于专栏:韩伟的专栏韩伟的专栏

推麻将的玩法

上一篇介绍了一个游戏运行的最基本结构,本篇开始根据一个具体的游戏,做一个游戏关卡。下面要做的是一个叫“推麻将”的桌面玩法。现在介绍一下这个玩法的具体内容:

  1. 一副麻将随机放在桌上,共 8 行 14 列
  2. 任何两个相同的麻将,直线相连如果没有其他麻将阻隔,就可以消除掉
  3. 桌上如果有空位(有麻将消除了留下的空位),相邻的四个方向的麻将行列,都可以整队移动;但是移动之后,被推动的这队麻将,必须至少要有一个能被消除的麻将,否则不能移动
  4. 桌上所有麻将都被消除完就是胜利;
  5. 消除和推动麻将的移动,使用鼠标点击来操作

第一个关卡

根据上篇设计的关卡基类 Scenario,我们可以为了一个特定的游戏,建立一个子类 MainScenario。

编写 MainScenario 也很简单,主要就是实现一个 start() 方法。此方法所需要做的事情,就是多次调用基类的 add_group() 方法,把需要显示的游戏对象,都以 Group 的组织形式,添加到关卡中去。

Group 对象及其内部的 Sprite 对象,一旦被 add_group() 放到 MainScenario 后,由于 Director 的 run() 方法,就会每帧(每秒60次的)去调用 MainScenario 的 update() 方法,因此在 MainScenario 中的 Group 对象,以及 Sprite 对象的 update() 方法也会被调用。所以我们游戏逻辑的主要实现代码就是:

  1. 编写 MainScenario.start() :放置游戏关卡初始的所有游戏对象组 Group 以及需要的游戏对象 Sprite
  2. 编写游戏对象 Group 和 Sprite 的子类,通过实现 __init__() 和 update() 方法来完成各种游戏行为
代码语言:javascript
复制
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 中。

然后根据游戏玩法我们设计了几个类,用来实现上述的玩法:

  1. Table:存放所有麻将的对象,会记录所有麻将的位置,每帧根据麻将的位置重绘画面,麻将移动过程也是通过 Table 显示。
  2. Mahjong:可以放在 Table 上显示,一个关卡中会有 8x14 共 112 个对象,每个对象保存自己的图案。点击麻将的事件处理也由此类处理。
  3. Edge:点击麻将后,显示的“选中”框,通过 effect 这个 Group 显示。Table 对象会记录 Edge 的位置,以记录当前选定的麻将。
  4. Point:桌面背景层,通过 bg 这个 Group 显示。点击 Point 会触发麻将的移动逻辑。桌面上也是由 112 个 Point 对象组成,因此被点击的 Point 是可以知道其坐标位置的。
  5. 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 对象,其他所有的可显示对象,在处理“被鼠标点击”事件的时候,都能获得完整的所有麻将的状态,非常方便编写游戏业务逻辑。

加载图像资源

在处理完“桌子”之后,下来需要处理的最复杂的资源,就是麻将了。一般来说,游戏的图像资源,都是一个图片文件。很多图像都拼接在同一个文件上,如下图:

每个麻将需要获得这个文件中图像的某一块,需要有两个步骤:

  1. 把整个图片加载到内存中,变成一个对象(变量)
  2. 截取自己需要的那一部分图像,变成一个对象,存放到 Mahjong 对象的 image 属性和 Rect 属性上。image 属性是 Sprite 基类规定了,用来显示的图像内容属性。而 Rect 属性则决定此 Sprite 对象显示在屏幕上的位置和大小。
代码语言:javascript
复制
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() 必须要使用随机数来实现这个功能:

  1. 用一个数组 mahjiongs 存放“未放入”的麻将堆
  2. 用一个数组 random_symbol 存放“打乱顺序”的麻将堆
  3. 随机从 mahjiongs 抽出一个麻将,加入到 random_symbol 中,直到 mahjiongs 变空
  4. 用 random_symbol 的顺序,一个个放入 Table 的 14x8 的数组 heap 中。
代码语言:javascript
复制
    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。

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

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

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

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

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