torinaブログ

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

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

Python Kivy Kivy
約111日前 2016年11月8日15:29
Kivyで、シンプルなシューティングゲーム①
https://torina.top/main/308/

の続きです。
今回は敵キャラと、スコア表示欄を追加しました。
Python3.4
Kivy1.9.1
です。

敵が上から襲ってくるようになりました。



敵に弾を当てると敵が消え、右側のスコアが加算されます。


敵が下まで行くとスコアが減ります。シンプルですね。



main.py
from random import randint
from kivy.app import App
from kivy.clock import Clock
from kivy.properties import (
    ListProperty,
    NumericProperty,
    ObjectProperty,
    StringProperty,
    ReferenceListProperty,
)
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget
from kivy.vector import Vector


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

    pass


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

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

    # 経験値
    experience = NumericProperty(100)

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

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


class Shot(Widget):
    """弾"""

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

    def move(self):
        """弾の移動処理"""

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


class ShootingArea(Widget):
    """シューティングエリア。画面の左側"""

    pass


class ScoreArea(BoxLayout):
    """スコアなど表示エリア。画面右側"""

    score = NumericProperty(0)
    info_text = StringProperty('Simple Shooting')


class ShootingGame(BoxLayout):
    """ルートウィジェット。シューティングゲーム全体の管理"""

    shooting_area = ObjectProperty(None)
    score_area = ObjectProperty(None)
    player = ObjectProperty(None)
    shots = ListProperty()
    enemies = ListProperty()

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

        if 0 < touch.x < self.shooting_area.width:
            self.player.center_x = touch.x

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

        shot = Shot(pos=self.player.center)
        self.shots.append(shot)
        self.shooting_area.add_widget(shot)

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

        各ショットの移動処理や、敵キャラの移動、それぞれの衝突判定を行う
        """

        # 弾の移動処理
        for ball in self.shots:
            ball.move()

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

            # 各弾が敵と衝突したら、敵を消してスコア加算
            for enemy in self.enemies:
                if enemy.collide_widget(ball):
                    self.shooting_area.remove_widget(enemy) 
                    self.enemies.remove(enemy)
                    self.score_area.score += enemy.experience

        # 敵の移動処理
        for enemy in self.enemies:
            enemy.move()

            # 敵が画面下まで達したら、敵を消してスコア-1000
            if enemy.y < self.shooting_area.y:
                self.shooting_area.remove_widget(enemy) 
                self.enemies.remove(enemy)
                self.score_area.score += -1000

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

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

        # 敵の生成処理
        enemy = Enemy(pos=(x, y))
        self.enemies.append(enemy)
        self.shooting_area.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

ShootingApp().run()


shooting.kv
<Player>:
    size: 100, 100
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size
            source: 'player.png'

<Enemy>:
    size: 50, 50
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size
            source: 'enemy.png'

<Shot>:
    size: 10, 30 
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size 

<ShootingArea>:
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size
            source: 'background.jpg'

<ScoreArea>:
    orientation: 'vertical'
    canvas:
        Color:
            rgba: 0, 0, .4, 1
        Rectangle:
            pos: self.pos
            size: self.size
    Label:
        text: str(root.score)

    Label:
        text: root.info_text 

<ShootingGame>:
    player: player
    shooting_area: shooting_area
    score_area: score_area
    orientation: 'horizontal'

    ShootingArea:
        id: shooting_area
        size_hint: .7, 1

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

    ScoreArea:
        id: score_area
        size_hint: .3, 1    


kvファイルにいくつかルール(<>で囲ってるもの)が増えました。
Enemyは敵キャラで、50x50で画像を読み込んでいます。
ShootingAreaは画面左の、遊ぶエリアですね。
ScoreAreaは画面右側のスコアが表示された部分です。
<Enemy>:
    size: 50, 50
	canvas:
	    Rectangle:
	        pos: self.pos
	        size: self.size
	        source: 'enemy.png'

<ShootingArea>:
	canvas:
	    Rectangle:
	        pos: self.pos
	        size: self.size
	        source: 'background.jpg'

<ScoreArea>:
    orientation: 'vertical'
    canvas:
        Color:
            rgba: 0, 0, .4, 1
        Rectangle:
            pos: self.pos
            size: self.size
    Label:
        text: str(root.score)

    Label:
        text: root.info_text 



ShootingGameをBoxLayoutにし、上のShootingArea、ScoreAreaを横に7:3で配置しました。
<ShootingGame>:
	player: player
	shooting_area: shooting_area
	score_area: score_area
	orientation: 'horizontal'

	ShootingArea:
	    id: shooting_area
	    size_hint: .7, 1

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

	ScoreArea:
                id: score_area
                size_hint: .3, 1


main.pyもいくつか変わりました。
ShootingAreaクラスですが、今回は特に処理は書きません。
class ShootingArea(Widget):
    """シューティングエリア。画面の左側"""

    pass


ScoreAreaクラスには、スコア表示用のプロパティとお知らせを表示するプロパティの2つを定義しました。
class ScoreArea(BoxLayout):
    """スコアなど表示エリア。画面右側"""

    score = NumericProperty(0)
    info_text = StringProperty('Simple Shooting')


敵キャラです。大体は弾と同じで、各種速度のプロパティと移動処理用のmoveメソッドがあります。
あと、経験値です。
class Enemy(Widget):
    """敵キャラ"""

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

    # 経験値
    experience = NumericProperty(100)

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

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



ShootingGameもいくつか変わりました。
shooting_area、score_area、enemies等です。
class ShootingGame(BoxLayout):
    """ルートウィジェット。シューティングゲーム全体の管理"""

    shooting_area = ObjectProperty(None)
    score_area = ObjectProperty(None)
    player = ObjectProperty(None)
    shots = ListProperty()
    enemies = ListProperty()



各弾の移動処理後に、敵キャラとの衝突判定をしています。
enemy.collide_widget(ball)は、敵ウィジェットと弾ウィジェットが重なるか?です。これはWidgetクラスで定義されており、Widgetを継承していれば使えます。
collide_pointという、指定の座標がWidgetの内側にあるか?という関数もあります。
その後は敵の移動処理と、敵が画面下まで行くとスコアを減らします。
    def update(self, dt):
        """1/60秒毎に呼ばれる、ゲーム更新処理

        各ショットの移動処理や、敵キャラの移動、それぞれの衝突判定を行う
        """

        # 弾の移動処理
        for ball in self.shots:
            ball.move()

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

            # 各弾が敵と衝突したら、敵を消してスコア加算
            for enemy in self.enemies:
                if enemy.collide_widget(ball):
                    self.shooting_area.remove_widget(enemy) 
                    self.enemies.remove(enemy)
                    self.score_area.score += enemy.experience

        # 敵の移動処理
        for enemy in self.enemies:
            enemy.move()

            # 敵が画面下まで達したら、敵を消してスコア-1000
            if enemy.y < self.shooting_area.y:
                self.shooting_area.remove_widget(enemy) 
                self.enemies.remove(enemy)
                self.score_area.score += -1000



もう一つ定期的に呼ぶ関数が増えました。1秒毎に敵を作成します。
randintで、x座標をランダムに決めています。
    def create_enemy(self, dt):
        """1秒毎に呼ばれる、敵キャラ作成処理"""

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

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


create_enemyはupdateと同様に、build関数内に追加しています。
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

]