人工知能に関する断創録

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

スプライトグループの使い方

スプライトの使い方(2008/5/17)の続きです。スプライトグループを使うと複数のスプライトをまとめて管理、更新、描画できるようになります。大量のスプライトを使うゲームでは非常に便利な機能です。サンプルコードは、スプライトの使い方(2008/5/17)とほとんど同じです。

group_test.zip

f:id:aidiary:20100605102045p:plain

サンプルスクリプト(1)スプライトグループ

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pygame
from pygame.locals import *
import sys
 
SCR_RECT = Rect(0, 0, 640, 480)
 
class MySprite(pygame.sprite.Sprite):
    def __init__(self, filename, x, y, vx, vy):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(filename).convert_alpha()
        width = self.image.get_width()
        height = self.image.get_height()
        self.rect = Rect(x, y, width, height)
        self.vx = vx
        self.vy = vy
        
    def update(self):
        self.rect.move_ip(self.vx, self.vy)
        # 壁にぶつかったら跳ね返る
        if self.rect.left < 0 or self.rect.right > SCR_RECT.width:
            self.vx = -self.vx
        if self.rect.top < 0 or self.rect.bottom > SCR_RECT.height:
            self.vy = -self.vy
        # 画面からはみ出ないようにする
        self.rect = self.rect.clamp(SCR_RECT)
 
def main():
    pygame.init()
    screen = pygame.display.set_mode(SCR_RECT.size)
    pygame.display.set_caption(u"スプライトグループの使い方")
    
    # スプライトを作成
    python1 = MySprite("python.png", 0, 0, 2, 2)
    python2 = MySprite("python.png", 10, 10, 5, 5)
    python3 = MySprite("python.png", 320, 240, -2, 3)
    
    # スプライトグループを作成してスプライトを追加
    group = pygame.sprite.RenderUpdates()
    group.add(python1)
    group.add(python2)
    group.add(python3)
    
    clock = pygame.time.Clock()
    
    while True:
        clock.tick(60)  # 60fps
        screen.fill((0,0,255))
        # スプライトグループを更新
        group.update()
        # スプライトグループを描画
        group.draw(screen)
        pygame.display.update()
        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit()
 
if __name__ == "__main__":
    main()

サンプルスクリプト(2)デフォルトスプライトグループ

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pygame
from pygame.locals import *
import sys
 
SCR_RECT = Rect(0, 0, 640, 480)
 
class MySprite(pygame.sprite.Sprite):
    def __init__(self, filename, x, y, vx, vy):
        # デフォルトグループをセット
        pygame.sprite.Sprite.__init__(self, self.containers)
        self.image = pygame.image.load(filename).convert_alpha()
        width = self.image.get_width()
        height = self.image.get_height()
        self.rect = Rect(x, y, width, height)
        self.vx = vx
        self.vy = vy
        
    def update(self):
        self.rect.move_ip(self.vx, self.vy)
        # 壁にぶつかったら跳ね返る
        if self.rect.left < 0 or self.rect.right > SCR_RECT.width:
            self.vx = -self.vx
        if self.rect.top < 0 or self.rect.bottom > SCR_RECT.height:
            self.vy = -self.vy
        # 画面からはみ出ないようにする
        self.rect = self.rect.clamp(SCR_RECT)
 
def main():
    pygame.init()
    screen = pygame.display.set_mode(SCR_RECT.size)
    pygame.display.set_caption(u"スプライトグループの使い方2")
    
    # スプライトグループを作成してスプライトクラスに割り当て
    group = pygame.sprite.RenderUpdates()
    MySprite.containers = group
    
    # スプライトを作成
    python1 = MySprite("python.png", 0, 0, 2, 2)
    python2 = MySprite("python.png", 10, 10, 5, 5)
    python3 = MySprite("python.png", 320, 240, -2, 3)
    
    clock = pygame.time.Clock()
    
    while True:
        clock.tick(60)  # 60fps
        screen.fill((0,0,255))
        # スプライトグループを更新
        group.update()
        # スプライトグループを描画
        group.draw(screen)
        pygame.display.update()
        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit()
 
if __name__ == "__main__":
    main()

サンプルスクリプト(3)汚れたRectのアニメーション

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pygame
from pygame.locals import *
import sys
 
SCR_RECT = Rect(0, 0, 640, 480)
 
class MySprite(pygame.sprite.Sprite):
    def __init__(self, filename, x, y, vx, vy):
        # デフォルトグループをセット
        pygame.sprite.Sprite.__init__(self, self.containers)
        self.image = pygame.image.load(filename).convert_alpha()
        width = self.image.get_width()
        height = self.image.get_height()
        self.rect = Rect(x, y, width, height)
        self.vx = vx
        self.vy = vy
        
    def update(self):
        self.rect.move_ip(self.vx, self.vy)
        # 壁にぶつかったら跳ね返る
        if self.rect.left < 0 or self.rect.right > SCR_RECT.width:
            self.vx = -self.vx
        if self.rect.top < 0 or self.rect.bottom > SCR_RECT.height:
            self.vy = -self.vy
        # 画面からはみ出ないようにする
        self.rect = self.rect.clamp(SCR_RECT)
 
def main():
    pygame.init()
    screen = pygame.display.set_mode(SCR_RECT.size)
    pygame.display.set_caption(u"スプライトグループの使い方2")
    
    # スプライトグループを作成してスプライトクラスに割り当て
    group = pygame.sprite.RenderUpdates()
    MySprite.containers = group
    
    # スプライトを作成
    python1 = MySprite("python.png", 0, 0, 2, 2)
    python2 = MySprite("python.png", 10, 10, 5, 5)
    python3 = MySprite("python.png", 320, 240, -2, 3)
    
    clock = pygame.time.Clock()
    
    # 背景の作成と描画(背景は最初に1回だけ描画)
    background = pygame.Surface(SCR_RECT.size)
    background.fill((0,0,255))
    screen.blit(background, (0,0))
    pygame.display.update()
    
    while True:
        clock.tick(60)  # 60fps
        # 背景の全体描画はしない!
        # screen上のSpriteを背景で消去
        group.clear(screen, background)
        # スプライトグループを更新
        group.update()
        print python1.rect
        # スプライトグループを描画
        # RenderUpdateのdraw()は変化があった部分の矩形(dirty rect)を返す
        dirty_rects = group.draw(screen)
        print dirty_rects
        # updateにdirty rectを渡すとその部分だけ更新するので効率よい
        pygame.display.update(dirty_rects)
        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit()
 
if __name__ == "__main__":
    main()

スプライトグループの作成とスプライトの追加

まずはスプライトグループを作成して、グループにスプライトを追加する処理です。

    # スプライトを作成
    python1 = MySprite("python.png", 0, 0, 2, 2)
    python2 = MySprite("python.png", 10, 10, 5, 5)
    python3 = MySprite("python.png", 320, 240, -2, 3)
    
    # スプライトグループを作成してスプライトを追加
    group = pygame.sprite.RenderUpdates()
    group.add(python1)
    group.add(python2)
    group.add(python3)

スプライトグループには用途によっていくつか種類があります。pygame.sprite.Groupは、もっとも基本的なスプライトグループで主に衝突判定に使われることが多いようです。今回は衝突判定は行わないのでこのスプライトグループは使いません。もう1つは、Groupを継承しているpygame.sprite.RenderUpdatesです。こちらは、主にスプライト群の更新と描画に使います。今回はこっちを使います。

サンプルでは、RenderUpdatesオブジェクトを作成して、Group.add()で3つのスプライトをグループに追加しています。ドキュメントには RenderUpdatesのadd()なんて載ってない!と思われるかもしれませんが、RenderUpdatesはGroupを継承しているので Group.add()がそのまま使えます。

Pygameの古い解説を読むとpygame.sprite.RenderPlainというのを使っていることがあります。このクラスはまだ使えるようですがドキュメントに載っていません。互換性のため用意されている古い機能のようです。かわりにRenderUpdatesを使った方がよさそうです。

スプライトグループによる更新と描画

        group.update()              # スプライトグループを更新
        group.draw(screen)       # スプライトグループを描画

複数のスプライトをグループにまとめると更新と描画がシンプルになります。Group.update()とGroup.draw()を使うことでグループに含まれるスプライトをまとめて更新、描画できます。スプライトの使い方と比較してみてください。

デフォルトスプライトグループ

サンプルスクリプト(2)のようにあらかじめスプライトのデフォルトグループを指定しておいて、スプライトの作成と同時にグループに追加することもできます。Spriteの引数にグループリストを渡すことができます。

class MySprite(pygame.sprite.Sprite):
    def __init__(self, filename, x, y, vx, vy):
        pygame.sprite.Sprite.__init__(self, self.containers)  # Spriteグループを指定

self.containersなんてMySpriteのどこにも定義されてないと思ったかもしれませんが、これはクラスの外部でセットしています。

    # スプライトグループを作成してスプライトクラスに割り当て
    group = pygame.sprite.RenderUpdates()
    MySprite.containers = group  # MySpriteのデフォルトグループをセット
    
    # スプライトを作成
    python1 = MySprite("python.png", 0, 0, 2, 2)
    python2 = MySprite("python.png", 10, 10, 5, 5)
    python3 = MySprite("python.png", 320, 240, -2, 3)

こうするとスプライトオブジェクトを作成すると同時に自動的に指定したグループ(サンプルではgroup)に追加されます。実際、スプライトを作成した後にグループにadd()で追加してないですよね。この方法を使う場合は、スプライトを作成する前に containersをセットしておく必要があります。ちなみにデフォルトグループは複数指定することもできます。

    MySprite.containers = group1, group2, group3

この書き方はPygameのチュートリアルを見るとよく出てくるので覚えておくとよいと思います。ただ、 containersをクラスの外部から指定するなどプログラミング作法としてはあまりよくないなぁと思ってしまいます。他の言語ではこういう書き方はあまりしないため、Pythonっぽさがにじみ出ています。慣れると何てことないんですが、慣れるまで少し大変でした。

スプライトグループには衝突判定などまだまだ機能がありますが、おいおい解説していきます。

汚れたRectのアニメーション(dirty rect animation)

スプライトの描画を高速に行うために汚れたRectのアニメーション(dirty rect animation)というテクニックがあります。簡単に言うと毎フレームごとに画面全体を描き換えるのではなく、スプライトが更新された一部分だけ描き換えるテクニックです。画面全体を描き換えるより描画が高速になります。

サンプルスクリプト(3)が汚れたRectのアニメーションのサンプルです。このテクニックを使うには背景をSurfaceにする必要があるので青色単色で塗りつぶしたSurfaceを用意して、ゲームループに入る前に1度だけ描画しておきます。

    # 背景の作成と描画(背景は最初に1回だけ描画)
    background = pygame.Surface(SCR_RECT.size)
    background.fill((0,0,255))
    screen.blit(background, (0,0))
    pygame.display.update()

汚れたRectのアニメーションの手順は以下のようになります。

  1. ゲームループの前に背景を描画
  2. スプライトを背景画像を使って消去
  3. スプライトを移動
  4. スプライトを描画
  5. 変化があった部分のRectを取得
  6. 変化があった部分だけ更新

ゲームループでは、上記の手順に従っています。ポイントは、RenderUpdatesのdraw()が変化があった部分の Rect(dirty rect)を返すってこととRenderUpdateのupdate()にdirty rectを渡すとその部分だけ更新されるってことです。

    while True:
        clock.tick(60)  # 60fps
        # 背景の全体描画はしない!
        # screen上のSpriteを背景で消去
        group.clear(screen, background)
        # スプライトグループを更新
        group.update()
        # スプライトグループを描画
        # RenderUpdateのdraw()は変化があった部分の矩形(dirty rect)を返す
        dirty_rects = group.draw(screen)
        # updateにdirty rectを渡すとその部分だけ更新するので効率よい
        pygame.display.update(dirty_rects)

このテクニックが有効なのは、背景が固定でスプライトをたくさん動かすゲームです。ブロック崩しとか弾幕シューティングとかでしょうか。背景全体がスクロールするゲーム、たとえば横スクロールシューティングとかでは効果がありません。RPGも移動のたびにスクロールしなくてはいけないので効果がないです。

最近のPCは昔と違って桁違いに速くなっているので最初から無理に汚れたRectを気にする必要はあまりないと思います。とりあえず全体を描画してしまって遅くてダメだと思ったら汚れたRectで最適化するという姿勢でいいのではないでしょうか?少なくともこのサイトで作ろうとしている2Dゲームでは使わなくても大丈夫でした。