人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

ブロックとの衝突判定

ブロックを作成します。パイソンはブロックと衝突するのでぶつかったり、乗ったりできるようになります。

block.zip
f:id:aidiary:20100812210229p:plain

ブロッククラス

まずブロックを表すクラスを作成します。ブロックもスプライトなのでSpriteを継承して作ります。

class Block(pygame.sprite.Sprite):
    """ブロック"""
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self, self.containers)
        self.rect = self.image.get_rect()
        self.rect.topleft = pos

すごく簡単なクラスです。ブロックの位置と大きさをself.rectで管理し、コンストラクタの引数posの位置に表示されるようにしています。今まで作ってきた他のスプライトと同様に画像であるself.imageとデフォルトスプライトグループであるself.containersは外部からセットします。これはPyActionの__init__()で下のように設定してます。

        # 画像のロード
        Block.image = load_image("block.png", -1)

        # スプライトグループの作成
        self.all = pygame.sprite.RenderUpdates()
        self.blocks = pygame.sprite.Group()
        Block.containers = self.all, self.blocks

ブロックは描画用のallグループの他にblocksというグループにも登録しています。これは、パイソンとの衝突判定に使います。

ブロックの作成

実際にスクリーンショットにあるようなブロックを作成しているのはPyActionクラスのcreate_blocks()です。Blockはデフォルトスプライトグループを登録してあるのでオブジェクトを作成するだけで自動的にグループに登録され、描画や更新が行われます。Block()には座標をタプル形式で渡します。

    def create_blocks(self):
        """ブロックの作成"""
        # 天井と床
        for x in range(20):
            Block((x*32, 0))
            Block((x*32, 14*32))
        
        # 左右の壁
        for y in range(20):
            Block((0, y*32))
            Block((19*32, y*32))
        
        # 中央のトンネル
        Block((192,384)); Block((224,384)); Block((256,384))
        Block((288,384)); Block((320,384)); Block((352,384))
        
        # 右下にある山
        Block((480,416)); Block((512,416)); Block((544,416)); Block((576,416))
        Block((512,384)); Block((544,384)); Block((576,384))
        Block((544,352)); Block((576,352))
        Block((576,320))
        
        # 左下から右上への階段
        Block((32,384));  Block((128,320)); Block((224,256)); Block((384,192))
        Block((416,192)); Block((448,192)); Block((480,192))
        Block((512,192)); Block((544,192)); Block((576,192))
        
        # 左上のブロック
        Block((128, 128))

けっこうたくさん作るので疲れますね・・・これは、後でマップファイルやマップエディタを作ると簡単にできるようになります。

パイソンとブロックの衝突判定

この状態ではパイソンとブロックが衝突しないためぶつかったり上に乗ったりできません。そこで、パイソンがブロックにあたったら移動できないように衝突判定処理を加えます。PyActionクラスでPythonを作るときにblocksを渡してPythonクラス内でブロックとの衝突判定ができるようにしてます。

        # パイソンの作成
        # 衝突判定用にブロックグループを渡す
        Python((300,200), self.blocks)

次に、Pythonクラスのupdate()を見てください。ここで、X方向の衝突判定を行うcollision_x()とY方向の衝突判定を行うcollision_y()を呼び出しています。衝突判定はX方向とY方向をわけて考えると理解しやすいです。

    def update(self):
        """スプライトの更新"""

        # X方向の衝突判定処理
        self.collision_x()
        
        # この時点でX方向に関しては衝突がないことが保証されてる
        
        # Y方向の衝突判定処理
        self.collision_y()

まず、X方向の衝突判定処理です。たとえば、横に移動してってブロックに当たる場合ですね。この関数ではY方向の移動はまったく考えません。

    def collision_x(self):
        """X方向の衝突判定処理"""
        # パイソンのサイズ
        width = self.rect.width
        height = self.rect.height
        
        # X方向の移動先の座標と矩形を求める
        newx = self.fpx + self.fpvx
        newrect = Rect(newx, self.fpy, width, height)
        
        # ブロックとの衝突判定
        for block in self.blocks:
            collide = newrect.colliderect(block.rect)
            if collide:  # 衝突するブロックあり
                if self.fpvx > 0:    # 右に移動中に衝突
                    # めり込まないように調整して速度を0に
                    self.fpx = block.rect.left - width
                    self.fpvx = 0
                elif self.fpvx < 0:  # 左に移動中に衝突
                    self.fpx = block.rect.right
                    self.fpvx = 0
                break  # 衝突ブロックは1個調べれば十分
            else:
                # 衝突ブロックがない場合、位置を更新
                self.fpx = newx

まず、今のX方向の速度で移動したときの移動先X座標newxとその場所でのパイソンの矩形newrectを求めます。self.fpx、self.rectを更新しないので実際に移動はしてません。次にその移動先X座標に衝突するブロックがあるか調べます。この判定は、ブロックグループblocks内のすべてのブロックに対して行います。Rect同士の衝突判定はRect.colliderect()という関数を使っています。この関数は、R1.colliderect(R2)という形で使います。R1とR2はRectオブジェクト(矩形範囲)です。もし、R1とR2の矩形範囲が衝突していたらTrueを返します。ブロック崩し編のボールの反射(2008/8/10)でもボールとパドルの衝突判定に使いました。

もし衝突するブロックがある場合は、パイソンがブロックに対してどっちの方向から衝突しているか調べます。これは、X方向速度の正負を調べれば簡単ですね。もし、速度が正なら右へ移動中で負なら左へ移動中のはずです。どちらから衝突しているかわかったら下の図のようにブロックへめり込まないように位置を調節して速度を0にします。この段階でパイソンと衝突するブロックはなくなります。

f:id:aidiary:20100812210230p:plain

もし衝突するブロックが1つもなかった場合は、移動先のX座標でself.fpxをそのまま更新します。self.fpxを更新するので実際に移動します。

同じように、Y方向の衝突判定処理も作ります。たとえば、下に落ちてってブロックに当たる場合ですね。この関数ではX方向の移動はまったく考えません。

    def collision_y(self):
        """Y方向の衝突判定処理"""
        # パイソンのサイズ
        width = self.rect.width
        height = self.rect.height
        
        # Y方向の移動先の座標と矩形を求める
        newy = self.fpy + self.fpvy
        newrect = Rect(self.fpx, newy, width, height)
        
        # ブロックとの衝突判定
        for block in self.blocks:
            collide = newrect.colliderect(block.rect)
            if collide:  # 衝突するブロックあり
                if self.fpvy > 0:    # 下に移動中に衝突
                    # めり込まないように調整して速度を0に
                    self.fpy = block.rect.top - height
                    self.fpvy = 0
                    # 下に移動中に衝突したなら床の上にいる
                    self.on_floor = True
                elif self.fpvy < 0:  # 上に移動中に衝突
                    self.fpy = block.rect.bottom
                    self.fpvy = 0
                break  # 衝突ブロックは1個調べれば十分
            else:
                # 衝突ブロックがない場合、位置を更新
                self.fpy = newy
                # 衝突ブロックがないなら床の上にいない
                self.on_floor = False

X方向とほとんど同じなのがわかると思います。衝突したブロックがある場合は、上方向に移動中にぶつかったか下方向に移動中にぶつかったかをY方向速度の正負で判断します。どちらに移動中かわかったら、めり込まないように下図のように位置を調整します。

f:id:aidiary:20100812210231p:plain

X方向の衝突判定とは少し違って、Y方向の衝突判定では着地しているかどうかも判断する必要があります。もし下に移動中に衝突したなら床の上にいるはずなのでon_floorをTrueにします。もし衝突するブロックがない場合は下方向にもブロックがないはずなので空中にいると判断してon_floorをFalseにします。

あと今回から↑キー以外にスペースキーでジャンプできるようにしてあります。スペースの方が押しやすいですね。