美文网首页pythonpygame游戏编程
08 pygame编程入门实践篇(下)

08 pygame编程入门实践篇(下)

作者: 易景漫游杨晓宏 | 来源:发表于2019-07-16 15:08 被阅读0次

    pygame编程入门之八:Making Games With Pygame2

    4. 游戏对象类

    一旦您加载了模块,并编写了资源处理函数,您就需要继续编写一些游戏对象了。这样做的方式相当简单,尽管一开始看起来很复杂。你为游戏中的每一种对象编写一个类,然后为对象创建这些类的实例。然后,您可以使用这些类的方法来操作对象,给对象一些动作和交互功能。所以你的游戏在伪代码中,会是这样的:

    #!/usr/bin/python
    
    # [load modules here]
    
    # [resource handling functions here]
    
    class Ball:
        # [ball functions (methods) here]
        # [e.g. a function to calculate new position]
        # [and a function to check if it hits the side]
    
    def main:
        # [initiate game environment here]
    
        # [create new object as instance of ball class]
        ball = Ball()
    
        while 1:
            # [check for user input]
    
            # [call ball's update function]
            ball.update()
    

    当然,这是一个非常简单的例子,您需要输入所有的代码,而不是那些带括号的注释。但是你应该有基本想法。把一个类放在一个类中,你把所有的函数都放在一个球上,包括init,它会创造出所有的球的属性,然后更新,它会把球移动到它的新位置,然后在这个位置上移动blitting到屏幕上。
    然后您可以为所有其他的游戏对象创建更多的类,然后创建它们的实例,这样您就可以在主函数和主程序循环中轻松地处理它们。与此形成对比的是,在主函数中启动球,然后有许多无类的函数来操作一个集合球对象,你将会看到为什么使用类是一个优势:它允许你把每个对象的所有代码放在一个地方;它使用对象更容易;它添加新对象和操作它们变得更加灵活。
    您可以简单地为每个新球对象创建球类的新实例,而不是为每个新球对象添加更多的代码。魔法!

    4.1. 一个简单的球类

    这里有一个简单的类,它具有创建球对象所必需的功能,如果在主程序中调用update函数,那么就可以在屏幕上移动:

    class Ball(pygame.sprite.Sprite):
        """A ball that will move across the screen
        Returns: ball object
        Functions: update, calcnewpos
        Attributes: area, vector"""
    
        def __init__(self, vector):
            pygame.sprite.Sprite.__init__(self)
            self.image, self.rect = load_png('ball.png')
            screen = pygame.display.get_surface()
            self.area = screen.get_rect()
            self.vector = vector
    
        def update(self):
            newpos = self.calcnewpos(self.rect,self.vector)
            self.rect = newpos
    
        def calcnewpos(self,rect,vector):
            (angle,z) = vector
            (dx,dy) = (z*math.cos(angle),z*math.sin(angle))
            return rect.move(dx,dy)
    

    这里我们有球类,init球函数集,更新函数,改变了球的矩形在新的位置,和calcnewpos函数计算出球的新位置根据其当前位置,移动和向量。我马上就会解释物理。
    另一件需要注意的事情是文档字符串,这段时间稍微长一点,并解释了类的基础知识。这些字符串不仅对您自己和其他程序员来说很方便,而且还可以用于解析代码并记录代码的工具。它们不会对程序产生很大的影响,但是对于大的程序来说它们是无价的,所以这是一个很好的习惯。

    4.1.1. Diversion 1: Sprites

    为每个对象创建类的另一个原因是精灵。你在游戏中渲染的每一个图像都是一个精灵对象,因此,首先,每个对象的类都应该继承精灵类。这是Python类继承的一个很好的特性。现在,球类拥有所有与Sprite类一起的功能,并且球类的任何对象实例都将被Pygame注册为精灵。而对于文本和背景,它们不移动,可以把对象放在背景上,Pygame以不同的方式处理精灵对象,当我们查看整个程序的代码时,你会看到它。
    基本上,你为那个球创建一个球对象和一个精灵对象,然后你在sprite对象上调用球的更新函数,从而更新精灵。精灵还提供了复杂的方法来确定两个物体是否相撞。通常情况下,您可能只是在主循环中检查它们的矩形是否重叠,但这将涉及到大量的代码,这将是一种浪费,因为Sprite类提供了两个功能(spritecollide and groupcollide)来为您完成这项工作。

    4.1.2. Diversion 2: Vector physics

    除了球类的结构外,这段代码值得注意的是矢量物理,用来计算球的运动。任何涉及到角运动的游戏,除非你熟悉三角学,否则你不会走太远,所以我将介绍一些你需要知道的基础知识来理解calcnewpos函数。
    首先,你会注意到球有一个属性向量,它是由角和z组成的,这个角是用弧度来表示的,它会告诉你球运动的方向。Z是球运动的速度。所以通过这个向量,我们可以确定球的方向和速度,以及它在x轴和y轴上的移动程度:

    ../_images/tom_radians.png
    上面的图表说明了向量背后的基本数学。
    在左手图中,你可以看到球的投影运动是由蓝线表示的。这条线的长度(z)表示它的速度,角度是它移动的方向。球运动的角度总是从右边的x轴上取下,从这条线顺时针方向测量,如图所示。
    从球的角度和速度,我们可以算出它沿x轴和y轴移动了多少。因为Pygame不支持向量本身,我们只能通过沿着两个轴移动它的矩形来移动球。所以我们需要在x轴(dx)和y轴(dy)上解决这个角度和速度。这是一个简单的三角学问题,可以用图中所示的公式来完成。
    如果你以前学过基本的三角学知识,这对你来说都不应该是新闻。但是,为了防止健忘,这里有一些有用的公式可以记住,这将帮助你对角度进行视觉化(用度来表示角度比弧度更直观)。 ../_images/tom_formulae.png

    5. User-controllable objects

    到目前为止,你可以创建一个Pygame窗口,并渲染一个可以在屏幕上运行的球。
    下一步是制造一些用户可以控制的球拍。这可能比球简单得多,因为它不需要物理(除非你的用户控制的对象会以比上下更复杂的方式移动,比如像马里奥这样的平台角色,在这种情况下你需要更多的物理知识)。用户控制的对象很容易创建,这要归功于Pygame的事件队列系统,正如您将看到的。

    5.1. 一个简单的球拍类

    球拍类的原理与球类相似。你需要一个init函数来初始化这个球(这样你就可以为每只球拍创建一个对象实例),一个更新函数,在它被击到屏幕之前,在球棒上执行每帧的变化,以及定义这个类实际要做什么的功能。下面是一些示例代码:

    class Bat(pygame.sprite.Sprite):
        """Movable tennis 'bat' with which one hits the ball
        Returns: bat object
        Functions: reinit, update, moveup, movedown
        Attributes: which, speed"""
    
        def __init__(self, side):
            pygame.sprite.Sprite.__init__(self)
            self.image, self.rect = load_png('bat.png')
            screen = pygame.display.get_surface()
            self.area = screen.get_rect()
            self.side = side
            self.speed = 10
            self.state = "still"
            self.reinit()
    
        def reinit(self):
            self.state = "still"
            self.movepos = [0,0]
            if self.side == "left":
                self.rect.midleft = self.area.midleft
            elif self.side == "right":
                self.rect.midright = self.area.midright
    
        def update(self):
            newpos = self.rect.move(self.movepos)
            if self.area.contains(newpos):
                self.rect = newpos
            pygame.event.pump()
    
        def moveup(self):
            self.movepos[1] = self.movepos[1] - (self.speed)
            self.state = "moveup"
    
        def movedown(self):
            self.movepos[1] = self.movepos[1] + (self.speed)
            self.state = "movedown"
    

    正如你所看到的,这个类与它的结构中的球类非常相似。
    但是每个函数的作用是不同的。首先,有一个reinit函数,它在回合结束时使用,而bat需要被设置回它的起始位置,任何属性都被设置回它们的必要值。
    接下来,球拍移动的方式比球要复杂一些,因为它的运动很简单(向上/向下),但它依赖于使用者告诉它移动,不像球在每一帧中不断移动。为了理解球的运动方式,看一个快速的图来显示事件的顺序是很有帮助的:

    ../_images/tom_event-flowchart.png
    这里发生的是控制球棒的人按下按钮,将球棒向上移动。主游戏循环的每个迭代(每一帧),关键是是否进行,球拍的状态属性对象被设置为“移动”,moveup函数将调用,导致球的y位置降低速度属性的值(在本例中,10)。换句话说,只要键盘被压住,球拍就会以每帧10个像素的速度向上移动屏幕。state属性还没有使用,但是在处理自旋还是想要一些有用的调试输出,也是很有用的。
    一旦玩家过去,第二组框被调用,球拍的状态属性对象将回到“静止”状态,和movepos属性将回到(0,0),这意味着当更新函数被调用时,它不会把球拍移动。所以当玩家松开按键时,球拍就会停止移动。简单!

    5.1.1. Diversion 3: Pygame events

    那么我们怎么知道玩家什么时候把按键按下,然后释放呢
    有了Pygame事件队列系统,年青人!这是一个非常容易使用和理解的系统,所以这不会花很长时间:)您已经在基本的Pygame程序中看到了事件队列,它用于检查用户是否退出了应用程序。移动球拍的代码就这么简单:

    for event in pygame.event.get():
        if event.type == QUIT:
            return
        elif event.type == KEYDOWN:
            if event.key == K_UP:
                player.moveup()
            if event.key == K_DOWN:
                player.movedown()
        elif event.type == KEYUP:
            if event.key == K_UP or event.key == K_DOWN:
                player.movepos = [0,0]
                player.state = "still"
    

    这里假设您已经创建了一个bat的实例,并调用了object player。
    您可以看到熟悉结构布局,它遍历Pygame事件队列中每个事件,并用event.get()函数检索。当用户点击按键,按下鼠标按钮并移动操纵杆时,这些动作会被注入到Pygame事件队列中,然后直到处理。
    所以在主游戏循环的每次迭代中,你都要经历这些事件,检查它们是否是你想要处理的,然后适当地处理它们。在球拍身上的事件pump()函数。在每次迭代中调用update函数保持队列流。
    首先,我们检查用户是否退出了程序,如果他们退出了,就退出。然后我们检查是否有任何键被按下,如果是,我们检查它们是否是移动球拍的指定键。如果是,然后调用对应移动功能,并设置适当的bat状态(尽管 moveup movedown改变了moveup()和movedown()函数,这使得简洁的代码,并且不破坏封装,这意味着您将属性分配给对象本身,没有引用该对象的实例的名称)。
    注意这里我们有三个状态: still, moveup, and movedown。同样,如果您想要调试或计算旋转,这些都是很方便的。我们还会检查是否有任何键被“松开”(即不再被按住),如果是,我们就会阻止球拍移动。

    6. 把它们放在一起

    到目前为止,您已经学习了构建简单游戏所需的所有基础知识。您应该了解如何创建Pygame对象,Pygame如何显示对象,如何处理事件,以及如何使用物理将一些动作引入到您的游戏中。
    现在,我将展示如何将所有这些代码块放到游戏中。首先要做的是让球触到屏幕的两侧,让球棒能够击球,否则就不会有太多的比赛了。我们用Pygame的碰撞方法来做这个。

    6.1. 让球击中两边

    让它在两侧弹跳的基本原理很容易理解。你利用球的四个角坐标,检查它们是否与屏幕边缘的x或y坐标相对应。如果右上角和左上角都有y坐标为0,你就知道这个球现在在屏幕的最上面。在我们计算出了球的新位置之后,我们在更新函数中做了所有这些。

    if not self.area.contains(newpos):
          tl = not self.area.collidepoint(newpos.topleft)
          tr = not self.area.collidepoint(newpos.topright)
          bl = not self.area.collidepoint(newpos.bottomleft)
          br = not self.area.collidepoint(newpos.bottomright)
          if tr and tl or (br and bl):
                  angle = -angle
          if tl and bl:
                  self.offcourt(player=2)
          if tr and br:
                  self.offcourt(player=1)
    
    self.vector = (angle,z)
    

    检查这个区域是否包含了球的新位置(它总是应该的,我们不需要有else子句,尽管在其他情况下你可能想要考虑它。)
    然后检查四个角的坐标是否与该区域的边发生碰撞,并为每个结果创建对象。如果是的话,对象的值是1,或者是真值。如果不,那么价值将是零,或者是假的。
    然后我们看它是否击中了顶部或底部,如果是,它改变了球的方向。使用弧度,我们可以简单地改变它的正/负的值来做到这一点。还会检查球是否从侧面消失了,如果它有的话,我们会调用offcourt函数。在游戏中,重新设置球,在调用该函数时指定的玩家的分数增加1点,并显示新分数。
    最后,根据新的角度重新编译向量。就这样。球将欢快地从墙上弹回来,并以优雅的姿态离开墙面。

    6.2. 让球碰到球拍

    把球打到球拍身上很类似,它会撞到屏幕的两侧。仍然使用碰撞法,但是这次要检查球的矩形和球拍是否碰撞。在这段代码中,还添加了一些额外的代码来避免各种故障。您会发现,为了避免出现小故障和bug,您必须添加各种额外的代码,因此习惯了它是一件好事。

    else:
        # Deflate the rectangles so you can't catch a ball behind the bat
        player1.rect.inflate(-3, -3)
        player2.rect.inflate(-3, -3)
    
        # Do ball and bat collide?
        # Note I put in an odd rule that sets self.hit to 1 when they collide, and unsets it in the next
        # iteration. this is to stop odd ball behaviour where it finds a collision *inside* the
        # bat, the ball reverses, and is still inside the bat, so bounces around inside.
        # This way, the ball can always escape and bounce away cleanly
        if self.rect.colliderect(player1.rect) == 1 and not self.hit:
            angle = math.pi - angle
            self.hit = not self.hit
        elif self.rect.colliderect(player2.rect) == 1 and not self.hit:
            angle = math.pi - angle
            self.hit = not self.hit
        elif self.hit:
            self.hit = not self.hit
    self.vector = (angle,z)
    

    用另一段语句开始这部分,因为这是前面的代码块中执行的,以检查球是否碰到了边。
    如果它没有击中两边,它可能会击中一个球棒,所以继续进行条件。第一个故障修复是在这两个维度缩小球员矩形3像素,停止背后的球拍抓球(如果你想象你只是把球拍这球跟踪,矩形重叠,所以通常球将被“打击”)。
    接下来检查这些矩形是否会发生碰撞,还有一个小故障。请注意,我已经对这些奇怪的代码进行了注释——对于那些查看代码的人来说,解释一些不寻常的代码总是好的,因此当看到它的时候,您就会理解它。如果没有修复,球可能会击中球棒的一角,改变方向,一帧后仍然会发现自己在球拍内。然后它会认为它再次被击中了,并改变了它的方向。这种情况可能会发生几次,使得球的运动完全不真实。
    所以我们有一个变量,self.click,当它被击中时,我们将它设置为True,然后在后面加上一个False。当我们检查这些矩形是否发生碰撞时,我们也检查是否self命中是true/false,以阻止内部的反弹。
    这里的代码很容易理解。所有矩形都有一个碰撞函数,你可以在其中输入另一个物体的矩形,如果这些矩形是重叠的,如果不是,它就会返回True。我们可以通过从pi中减去当前的角度来改变方向(同样,你可以用弧度来做一个简单转变,它会把角度调整90度,然后把它往正确的方向发送;你可能会发现,在这一点上,对弧度的彻底理解是有道理的!)为了完成故障检查,我们换了self.hit。如果们被击中后的框架,那就返回False。
    然后重新编译这个向量。当然,您希望删除前一段代码中的同一行,这样您只需要在if-else条件语句之后才做一次。这是它!合并后的代码将允许球击中两侧和球拍。

    6.3. 成品

    最终的产品,加上所有的代码块,以及其他一些代码将它们整合在一起,看起来就像这样:

    #
    # Tom's Pong
    # A simple pong game with realistic physics and AI
    # http://www.tomchance.uklinux.net/projects/pong.shtml
    #
    # Released under the GNU General Public License
    
    VERSION = "0.4"
    
    try:
        import sys
        import random
        import math
        import os
        import getopt
        import pygame
        from socket import *
        from pygame.locals import *
    except ImportError, err:
        print "couldn't load module. %s" % (err)
        sys.exit(2)
    
    def load_png(name):
        """ Load image and return image object"""
        fullname = os.path.join('data', name)
        try:
            image = pygame.image.load(fullname)
            if image.get_alpha is None:
                image = image.convert()
            else:
                image = image.convert_alpha()
        except pygame.error, message:
            print 'Cannot load image:', fullname
            raise SystemExit, message
        return image, image.get_rect()
    
    class Ball(pygame.sprite.Sprite):
        """A ball that will move across the screen
        Returns: ball object
        Functions: update, calcnewpos
        Attributes: area, vector"""
    
        def __init__(self, (xy), vector):
            pygame.sprite.Sprite.__init__(self)
            self.image, self.rect = load_png('ball.png')
            screen = pygame.display.get_surface()
            self.area = screen.get_rect()
            self.vector = vector
            self.hit = 0
    
        def update(self):
            newpos = self.calcnewpos(self.rect,self.vector)
            self.rect = newpos
            (angle,z) = self.vector
    
            if not self.area.contains(newpos):
                tl = not self.area.collidepoint(newpos.topleft)
                tr = not self.area.collidepoint(newpos.topright)
                bl = not self.area.collidepoint(newpos.bottomleft)
                br = not self.area.collidepoint(newpos.bottomright)
                if tr and tl or (br and bl):
                    angle = -angle
                if tl and bl:
                    #self.offcourt()
                    angle = math.pi - angle
                if tr and br:
                    angle = math.pi - angle
                    #self.offcourt()
            else:
                # Deflate the rectangles so you can't catch a ball behind the bat
                player1.rect.inflate(-3, -3)
                player2.rect.inflate(-3, -3)
    
                # Do ball and bat collide?
                # Note I put in an odd rule that sets self.hit to 1 when they collide, and unsets it in the next
                # iteration. this is to stop odd ball behaviour where it finds a collision *inside* the
                # bat, the ball reverses, and is still inside the bat, so bounces around inside.
                # This way, the ball can always escape and bounce away cleanly
                if self.rect.colliderect(player1.rect) == 1 and not self.hit:
                    angle = math.pi - angle
                    self.hit = not self.hit
                elif self.rect.colliderect(player2.rect) == 1 and not self.hit:
                    angle = math.pi - angle
                    self.hit = not self.hit
                elif self.hit:
                    self.hit = not self.hit
            self.vector = (angle,z)
    
        def calcnewpos(self,rect,vector):
            (angle,z) = vector
            (dx,dy) = (z*math.cos(angle),z*math.sin(angle))
            return rect.move(dx,dy)
    
    class Bat(pygame.sprite.Sprite):
        """Movable tennis 'bat' with which one hits the ball
        Returns: bat object
        Functions: reinit, update, moveup, movedown
        Attributes: which, speed"""
    
        def __init__(self, side):
            pygame.sprite.Sprite.__init__(self)
            self.image, self.rect = load_png('bat.png')
            screen = pygame.display.get_surface()
            self.area = screen.get_rect()
            self.side = side
            self.speed = 10
            self.state = "still"
            self.reinit()
    
        def reinit(self):
            self.state = "still"
            self.movepos = [0,0]
            if self.side == "left":
                self.rect.midleft = self.area.midleft
            elif self.side == "right":
                self.rect.midright = self.area.midright
    
        def update(self):
            newpos = self.rect.move(self.movepos)
            if self.area.contains(newpos):
                self.rect = newpos
            pygame.event.pump()
    
        def moveup(self):
            self.movepos[1] = self.movepos[1] - (self.speed)
            self.state = "moveup"
    
        def movedown(self):
            self.movepos[1] = self.movepos[1] + (self.speed)
            self.state = "movedown"
    
    
    def main():
        # Initialise screen
        pygame.init()
        screen = pygame.display.set_mode((640, 480))
        pygame.display.set_caption('Basic Pong')
    
        # Fill background
        background = pygame.Surface(screen.get_size())
        background = background.convert()
        background.fill((0, 0, 0))
    
        # Initialise players
        global player1
        global player2
        player1 = Bat("left")
        player2 = Bat("right")
    
        # Initialise ball
        speed = 13
        rand = ((0.1 * (random.randint(5,8))))
        ball = Ball((0,0),(0.47,speed))
    
        # Initialise sprites
        playersprites = pygame.sprite.RenderPlain((player1, player2))
        ballsprite = pygame.sprite.RenderPlain(ball)
    
        # Blit everything to the screen
        screen.blit(background, (0, 0))
        pygame.display.flip()
    
        # Initialise clock
        clock = pygame.time.Clock()
    
        # Event loop
        while 1:
            # Make sure game doesn't run at more than 60 frames per second
            clock.tick(60)
    
            for event in pygame.event.get():
                if event.type == QUIT:
                    return
                elif event.type == KEYDOWN:
                    if event.key == K_a:
                        player1.moveup()
                    if event.key == K_z:
                        player1.movedown()
                    if event.key == K_UP:
                        player2.moveup()
                    if event.key == K_DOWN:
                        player2.movedown()
                elif event.type == KEYUP:
                    if event.key == K_a or event.key == K_z:
                        player1.movepos = [0,0]
                        player1.state = "still"
                    if event.key == K_UP or event.key == K_DOWN:
                        player2.movepos = [0,0]
                        player2.state = "still"
    
            screen.blit(background, ball.rect, ball.rect)
            screen.blit(background, player1.rect, player1.rect)
            screen.blit(background, player2.rect, player2.rect)
            ballsprite.update()
            playersprites.update()
            ballsprite.draw(screen)
            playersprites.draw(screen)
            pygame.display.flip()
    
    
    if __name__ == '__main__': main()
    

    除了展示最终产品,我还会把你们带回到TomPong上,所有这些都是基于此的。
    下载,看看源代码,你会看到一个全面实施pong使用的所有代码。在本教程中,您看到的以及很多其他的代码我已经添加各种版本,比如一些额外的物理旋转,和其他各种错误和故障修复。

    相关文章

      网友评论

        本文标题:08 pygame编程入门实践篇(下)

        本文链接:https://www.haomeiwen.com/subject/hohtlctx.html