torinaブログ

DjangoとBootstrap4で作成したブログ
Python, Django, Kivy, Bootstrap, Apache等のメモです
ソースコード

Kivyで、シンプルなシューティングゲーム③

Python Kivy Kivy
約99日前 2016年11月13日8:43
Kivyで、シンプルなシューティングゲーム②
https://torina.top/main/309/

から少し進化し、敵も弾を撃つようになりました。
また、敵を倒したりすると爆発(というか火)するようになりました。


画像素材は以下から借りました。
http://sore.hontonano.jp/objectandelement/916-elegant-flame-set.html
http://sore.hontonano.jp/objectandelement/744-star-hoshi.html

main.py
from random import randint
from kivy.app import App
from kivy.clock import Clock
from kivy.properties import ObjectProperty
from kivy.uix.boxlayout import BoxLayout
from widget import Player, Enemy


class ShootingGame(BoxLayout):
    """ルートウィジェット"""

    main = ObjectProperty(None)
    menu = ObjectProperty(None)

    def update(self, dt):
        """1秒に60回呼ばれるゲーム更新処理"""

        for widget in self.main.children:
            widget.update()
        # print(len(self.main.children))

    def create_enemy(self, dt):
        """1秒毎に呼ばれる、敵キャラ作成処理"""

        # 敵キャラのx座標をランダムに。y座標は固定、一番上
        x = randint(0, self.main.width)
        y = self.main.top

        # 敵の生成処理
        enemy = Enemy(pos=(x, y))
        self.main.add_widget(enemy)


class ShootingApp(App):

    def build(self):
        game = ShootingGame()
        Clock.schedule_interval(game.update, 1.0 / 60.0)
        Clock.schedule_interval(game.create_enemy, 1.0)
        return game


if __name__ == '__main__':
    ShootingApp().run()



shooting.kv
<ShootingGame>:
    player: player
    main: main
    menu: menu
    orientation: 'horizontal'
 
    Widget:
        id: main
        size_hint: .7, 1

        canvas:
            Rectangle:
                pos: self.pos
                size: self.size
                source: 'background.jpg'

        Player:
            id: player
            pos: main.center_x - self.width/2, 0
 
    BoxLayout:
        id: menu
        size_hint: .3, 1
        orientation: 'vertical'

        canvas:
            Color:
                rgba: 0, 0, .4, 1
            Rectangle:
                pos: self.pos
                size: self.size

        Label:
            text: 'HITPOINT {}'.format(str(player.hitpoint))

        Label:
            text: 'SCORE {}'.format(str(player.score))


widget.py
from kivy.clock import Clock
from kivy.properties import (
    NumericProperty,
    ObjectProperty,
    ReferenceListProperty,
    StringProperty,
)
from kivy.uix.image import Image
from kivy.uix.widget import Widget
from kivy.vector import Vector


class ShootingWidget(Image):
    """シューティングゲームのウィジェット、基底クラス"""

    # updateが何回呼ばれたか
    update_count = NumericProperty(0)

    # ヒットポイント
    hitpoint = NumericProperty(100)

    # キャラ画像
    source = StringProperty('noimage.png')

    # 与える経験値
    point = NumericProperty(0)

    # 獲得したスコア
    score = NumericProperty(0)

    # 幅、高さ
    width = NumericProperty(100)
    height = NumericProperty(100)

    def update(self):
        """1秒間に60回呼ばれる更新処理。必要があればサブクラスで上書きすること"""

        pass

    def attack(self, target):
        """攻撃処理

        対象のヒットポイントを減らし、0以下になれば対象のdestroyを呼びます。
        また、自分のスコアに対象の経験値を加算します
        """

        target.hitpoint -= self.hitpoint
        if target.hitpoint <= 0:
            self.score = target.point
            target.destroy()

    def destroy(self):
        """ウィジェットの死亡処理

        create_fireで燃えカスを同じ場所に生成し、
        removeを呼び出し自分を画面から消去する
        """

        self.create_fire()
        self.remove()

    def create_fire(self):
        """燃えカスを生成する"""

        fire = Fire(pos=self.center)
        self.parent.add_widget(fire)    

    def remove(self):
        """自分を画面から消去する

        燃えカスを生成したくない場合や、ウィジェットが削除される前に何かの処理を挟みたい場合に
        上書きしてください。
        self.parent.remove_widgetの後に、
        self.parentを参照しないようにしてください。self.parentはNoneになっています
        """

        self.parent.remove_widget(self)


class Shot(ShootingWidget):
    """弾"""

    source = StringProperty('shot.png')
    point = NumericProperty(50)

    # 自分を発射したウィジェット
    shooter = ObjectProperty(None)

    # 速度
    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(10)
    velocity = ReferenceListProperty(velocity_x, velocity_y)

    def update(self):

        self.pos = Vector(*self.velocity) + self.pos

        # 弾が画面の上まで達したら消す
        if self.top > self.parent.top:
            self.remove()

        # 弾が画面の下まで達したら消す
        elif self.y < self.parent.y:
            self.remove()

        # 弾の通常処理
        else:
            # 画面内のウィジェットを取得する
            for widget in self.parent.children:

                # 重なっていればダメージを与え、自分を消去
                if self.is_collide(widget):
                    self.attack(widget)
                    self.remove()
                    break

    def is_collide(self, widget):
        """他のウィジェットと重なっていればTrue

        自分自身や、自分を発射したウィジェット、Fire(燃え後)ウィジェットを除き
        重なっていればTrueを返す
        """

        if widget is self:
            return False
        elif widget is self.shooter:
            return False
        elif isinstance(widget, Fire):
            return False
        elif not self.collide_widget(widget):
            return False
        return True

    def remove(self):
        """自分を画面から消去

        弾に溜まったスコアをシューターに還元する処理を追加するため、オーバーライド
        """

        self.shooter.score += self.score
        self.parent.remove_widget(self)


class Player(ShootingWidget):
    """プレイヤー"""

    source = StringProperty('player.png')
    hitpoint = NumericProperty(1000)
    point = NumericProperty(1000)
    width = NumericProperty(100)
    height = NumericProperty(100)

    def on_touch_move(self, touch):
        """タッチしたまま移動でプレイヤー移動"""

        if 0 < touch.x < self.parent.width:
            self.center_x = touch.x

    def on_touch_down(self, touch):
        """画面タッチで弾発射"""

        shot = Shot(shooter=self)
        x = self.center_x - shot.width / 2
        y = self.center_y
        shot.pos = x, y
        self.parent.add_widget(shot)


class Enemy(ShootingWidget):
    """敵キャラ"""

    source = StringProperty('enemy.png')
    point = NumericProperty(100)
    width = NumericProperty(50)
    height = NumericProperty(50)
    
    # 速度
    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(-1)
    velocity = ReferenceListProperty(velocity_x, velocity_y)

    def update(self):
        """敵キャラの移動処理"""

        self.update_count += 1
        self.pos = Vector(*self.velocity) + self.pos

        # 1秒ごとに弾発射
        if self.update_count % 60 == 0:
            self.shoot()

        # 画面の下まで到達したら、消える
        if self.y < self.parent.y:
            self.remove()

    def shoot(self):
        """弾を発射する"""

        # velocityを-にし、下方向へ発射
        shot = Shot(velocity_y=-5, shooter=self)
        x = self.center_x - shot.width / 2
        y = self.center_y - shot.height
        shot.pos = x, y
        self.parent.add_widget(shot)


class Fire(ShootingWidget):
    """燃え跡"""

    source = StringProperty('fire.png')
    width = NumericProperty(50)
    height = NumericProperty(50)

    def update(self):
        self.update_count += 1
        # 1秒経ったら、消える
        if self.update_count >= 60:
            return self.remove()



kvファイルを変更しました。
mainはメインとなるシューティングの画面、menuはスコアやHPを表示する画面です。
playerは自機ですね。
<ShootingGame>:
    player: player
    main: main
    menu: menu
    orientation: 'horizontal'


左7割がメインの画面です。canvasを使った背景の指定と、Playerの配置です。
Playerは、真ん中に入りするようposを指定します。
    Widget:
        id: main
        size_hint: .7, 1

        canvas:
            Rectangle:
                pos: self.pos
                size: self.size
                source: 'background.jpg'

        Player:
            id: player
            pos: main.center_x - self.width/2, 0


そして右側のスコア画面です。
色の指定と、Labelを2つ配置しています。
Playerのヒットポイントと、スコアです。
    BoxLayout:
        id: menu
        size_hint: .3, 1
        orientation: 'vertical'

        canvas:
            Color:
                rgba: 0, 0, .4, 1
            Rectangle:
                pos: self.pos
                size: self.size

        Label:
            text: 'HITPOINT {}'.format(str(player.hitpoint))

        Label:
            text: 'SCORE {}'.format(str(player.score))



main.pyも小さくなりました。
build内ではルートウィジェットのインスタンス化と、イベントの追加です。
game.updateは秒間60回呼ばれるゲームの各種更新処理、create_enemyは1秒毎の敵の作成処理です。
class ShootingApp(App):

    def build(self):
        game = ShootingGame()
        Clock.schedule_interval(game.update, 1.0 / 60.0)
        Clock.schedule_interval(game.create_enemy, 1.0)
        return game


if __name__ == '__main__':
    ShootingApp().run()



ルートウィジェットにはまずmainとmenuの参照を作り...
class ShootingGame(BoxLayout):
    """ルートウィジェット"""

    main = ObjectProperty(None)
    menu = ObjectProperty(None)


敵の作成処理は、x軸をランダムに、y軸は画面上に固定し敵を作ります。
    def create_enemy(self, dt):
        """1秒毎に呼ばれる、敵キャラ作成処理"""

        # 敵キャラのx座標をランダムに。y座標は固定、一番上
        x = randint(0, self.main.width)
        y = self.main.top

        # 敵の生成処理
        enemy = Enemy(pos=(x, y))
        self.main.add_widget(enemy)



self.main.childrenで、メイン画面内にあるウィジェットを全て取得します。
Player(自機)、敵、弾、燃えカス等です。全てにupdateメソッドを実装し、呼び出します。
    def update(self, dt):
        """1秒に60回呼ばれるゲーム更新処理"""

        for widget in self.main.children:
            widget.update()
        # print(len(self.main.children))


これはデバッグ用に使うと良さそうです。main画面内のウィジェットの数を返します。
これが明らかに多い、減らない、ということであれば、何らかのウィジェットがremoveされてないでしょう。
その場合、どんどん重たくなっていきます。
print(len(self.main.children))



widget.pyです。
まず、PlayerやEnemyが継承する基底クラスを作りました。
それぞれのクラスの変数は、コメントの通りです。
全てのウィジェットはhitpointやscore、pointを持つようにしてみました。
class ShootingWidget(Image):
    """シューティングゲームのウィジェット、基底クラス"""

    # updateが何回呼ばれたか
    update_count = NumericProperty(0)

    # ヒットポイント
    hitpoint = NumericProperty(100)

    # キャラ画像
    source = StringProperty('noimage.png')

    # 与える経験値
    point = NumericProperty(0)

    # 獲得したスコア
    score = NumericProperty(0)

    # 幅、高さ
    width = NumericProperty(100)
    height = NumericProperty(100)


このupdateが、main.pyから呼ばれます。移動処理などを実装することになるでしょう。
    def update(self):
        """1秒間に60回呼ばれる更新処理。必要があればサブクラスで上書きすること"""

        pass


攻撃処理です。現状はShotでしか使っていません。
自分のhitpointで相手のhitpointを減らし、0以下で自分のスコアを加算、相手をdestroyです。
    def attack(self, target):
        """攻撃処理

        対象のヒットポイントを減らし、0以下になれば対象のdestroyを呼びます。
        また、自分のスコアに対象の経験値を加算します
        """

        target.hitpoint -= self.hitpoint
        if target.hitpoint <= 0:
            self.score = target.point
            target.destroy()



死亡処理やリムーブ処理です。
destroyは壊されたとき等、removeは画面外に出たとき等、create_fireは被弾したときに呼び出したりしてもいいかもしれません。
self.parentは、今回はmainを参照できます。そのウィジェットが配置されているウィジェットが返されます。
親ウィジェットから自分をremoveすると、もう自分からself.parentの参照ができなくなる点に注意です。
    def destroy(self):
        """ウィジェットの死亡処理

        create_fireで燃えカスを同じ場所に生成し、
        removeを呼び出し自分を画面から消去する
        """

        self.create_fire()
        self.remove()

    def create_fire(self):
        """燃えカスを生成する"""

        fire = Fire(pos=self.center)
        self.parent.add_widget(fire)    

    def remove(self):
        """自分を画面から消去する

        燃えカスを生成したくない場合や、ウィジェットが削除される前に何かの処理を挟みたい場合に
        上書きしてください。
        self.parent.remove_widgetの後に、
        self.parentを参照しないようにしてください。self.parentはNoneになっています
        """

        self.parent.remove_widget(self)


弾ウィジェットです。画像とポイントを上書きし、自分を発射したshooter、各種速度を定義しています。
class Shot(ShootingWidget):
    """弾"""

    source = StringProperty('shot.png')
    point = NumericProperty(50)

    # 自分を発射したウィジェット
    shooter = ObjectProperty(None)

    # 速度
    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(10)
    velocity = ReferenceListProperty(velocity_x, velocity_y)



更新処理では、移動し、画面外からはみ出てればremoveで消えます。
for widget in self.parent.children:で、他のウィジェットを取り出し、重なっていれば攻撃し、自分は消えます。
    def update(self):

        self.pos = Vector(*self.velocity) + self.pos

        # 弾が画面の上まで達したら消す
        if self.top > self.parent.top:
            self.remove()

        # 弾が画面の下まで達したら消す
        elif self.y < self.parent.y:
            self.remove()

        # 弾の通常処理
        else:
            # 画面内のウィジェットを取得する
            for widget in self.parent.children:

                # 重なっていればダメージを与え、自分を消去
                if self.is_collide(widget):
                    self.attack(widget)
                    self.remove()
                    break


自分自身や、自分を撃ったウィジェット、燃えカスなど、重なっていても大丈夫なケースをここで定義しています。
    def is_collide(self, widget):
        """他のウィジェットと重なっていればTrue

        自分自身や、自分を発射したウィジェット、Fire(燃え後)ウィジェットを除き
        重なっていればTrueを返す
        """

        if widget is self:
            return False
        elif widget is self.shooter:
            return False
        elif isinstance(widget, Fire):
            return False
        elif not self.collide_widget(widget):
            return False
        return True



removeを上書きしました。attackを呼び出すのですが、弾自体にスコアが溜まってしまいます。
そのスコアをshooterに還元する必要がありました。
    def remove(self):
        """自分を画面から消去

        弾に溜まったスコアをシューターに還元する処理を追加するため、オーバーライド
        """

        self.shooter.score += self.score
        self.parent.remove_widget(self)



自機です。画像やヒットポイント等を上書きします。
class Player(ShootingWidget):
    """プレイヤー"""

    source = StringProperty('player.png')
    hitpoint = NumericProperty(1000)
    point = NumericProperty(1000)
    width = NumericProperty(100)
    height = NumericProperty(100)


移動と、弾発射処理です。Playerはタッチですぐ動くので、updateの実装は今回しませんでした。
    def on_touch_move(self, touch):
        """タッチしたまま移動でプレイヤー移動"""

        if 0 < touch.x < self.parent.width:
            self.center_x = touch.x

    def on_touch_down(self, touch):
        """画面タッチで弾発射"""

        shot = Shot(shooter=self)

        # 弾をウィジェットの中央に配置
        x = self.center_x - shot.width / 2
        y = self.center_y
        shot.pos = x, y
        self.parent.add_widget(shot)


敵キャラです。画像や幅、高さの上書きや、速度を定義しています。
class Enemy(ShootingWidget):
    """敵キャラ"""

    source = StringProperty('enemy.png')
    point = NumericProperty(100)
    width = NumericProperty(50)
    height = NumericProperty(50)
    
    # 速度
    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(-1)
    velocity = ReferenceListProperty(velocity_x, velocity_y)


updateは1秒間に60回呼ばれるので、60回呼ばれたら1秒経ったと考えることもできます。
移動し、1秒経っていれば弾発射、下まで到達で消えます。
self.update_count % 60 == 0:ではなく、Clock.schedule_intervalで1秒毎に呼び出しても良かったかもしれません。
    def update(self):
        """敵キャラの移動処理"""

        self.update_count += 1
        self.pos = Vector(*self.velocity) + self.pos

        # 1秒ごとに弾発射
        if self.update_count % 60 == 0:
            self.shoot()

        # 画面の下まで到達したら、消える
        if self.y < self.parent.y:
            self.remove()


弾の発射処理です。
    def shoot(self):
        """弾を発射する"""

        # velocityを-にし、下方向へ発射
        shot = Shot(velocity_y=-5, shooter=self)

        # 弾をウィジェットの中央に配置
        x = self.center_x - shot.width / 2
        y = self.center_y - shot.height
        shot.pos = x, y
        self.parent.add_widget(shot)


最後はFireクラスで、燃え後ウィジェットです。
敵や自分が壊されると、create_fireでこいつが生まれたりします。
1秒間には消える、儚い存在です。
class Fire(ShootingWidget):
    """燃え跡"""

    source = StringProperty('fire.png')
    width = NumericProperty(50)
    height = NumericProperty(50)

    def update(self):
        self.update_count += 1
        # 1秒経ったら、消える
        if self.update_count >= 60:
            return self.remove()


次はスタート画面や終了画面、各ステージの作成をします。
widget.pyももう少しジェネリックにしたいですね。