見出し画像

pyxelゲーム制作TIPS_シューティングゲーム風_後編

はじめに

このnoteはpythonのレトロゲームエンジン「pyxel」でのゲーム制作に役立つかもしれない小さなサンプルプログラムのTIPSです。
以下の環境で作成・動作させています。
・OS:Windows11
・開発環境:Visual Studio Code(Ver.1.88.1)
・pyxelのバージョン:2.0.12

記事内のコードはご自由にお使いください。イメージ等もファイルは添付していませんが、模写(?)して使っていただいて構いません。
また、pyxelの動作環境や各関数の詳細な説明はpyxel公式GitHubをご参照ください。

また、本noteは後編ですので、前編は以下のリンクからご確認ください。

作っていきましょう

それでは始めましょう。

前回はプレイヤーが攻撃できるようにするところまで進めました。
シューティングゲームであればやはり敵がいないと面白くありません。敵キャラクターを作っていきましょう。

▲敵キャラクターとなる赤い船の画像を用意。

さて、コードはまず敵キャラクターの設計図となるEnemyクラスを作成するところから始めます。

#敵クラス
class Enemy:
    def __init__(self):
        #ここでオブジェクト作成時の処理をします   
        self.t = pyxel.rndi(1, 2)
        if self.t == 1:
            self.x = pyxel.rndi(-60, -16)            
        elif self.t == 2:
            self.x = pyxel.rndi(180, 240)
        self.y = pyxel.rndi(16, 150)        
        self.speed = pyxel.rndi(1, 2)                

    def update(self):
        #各オブジェクトが毎フレーム行う更新処理です
        if self.t == 1:
            self.x += self.speed
        elif self.t == 2:
            self.x -= self.speed

    def draw(self):
        #各オブジェクトが毎フレーム行う描画処理です
        if self.t == 1:
            pyxel.blt(self.x, self.y, 0, 0, 48, 16, 16, 14)
        elif self.t == 2:
            pyxel.blt(self.x, self.y, 0, 0, 32, 16, 16, 14)

オブジェクト作成時に呼ばれるinit、座標更新用のupdate、描画用のdrawの3つのメソッド(関数)を備えたEnemyクラスを作成しました。
これに設定できる情報としては、自身の座標であるx、yの他に、進む向きを決めるt、スピードのspeedを作っています。x、y座標はそれぞれ適当な位置にランダムで設定するようにしています。
update部分では進む向きをtで判定して、speed分増やしたり減らしたりしています。右に進むほどx座標は大きくなるので、右に進めるときは+、左に進めるときは-です。
draw部分では同じく進む向きをtで判定して、描画を分けています。右向きと左向きの画像の切り替えですね。

        self.enemys = []

さて、クラスは完成しましたので、これを基にオブジェクトを作ります。その前に作ったオブジェクトの入れ物としてenemysの配列を作っておきましょう。これは本体Appクラスのinit部分に追記します。

        #敵の作成部分
        if pyxel.frame_count % pyxel.rndi(120, 240) == 0:
            new_enemy_count = pyxel.rndi(1, 3)
            for nw in range(new_enemy_count):
                self.enemys.append(Enemy())

敵の作成部分です。Appクラスのupdateに書きます。pyxel.frame_countはゲーム開始からの経過フレームを取得できる機能です。これを使って「ある程度時間が経ったら敵を追加」という動きを作ってみましょう。
「ある程度時間」と書きましたが、ここが毎回同じだと面白みに欠けるのでランダム性を持たせます。pyxel.rndi(120, 240)でランダム整数を取得します。if pyxel.frame_count % pyxel.rndi(120, 240) == 0:の条件文を付けることで「ランダム整数ぶんフレームが経過したら」という事になります。
条件文に入ったらnew_enemy_countで作成する敵の数を設定しましょう。ここでは「最低1体、最大3体」としています。
ここまで出来たらfor文でnew_enemy_countぶん処理をループさせてEnemyクラスのインスタンスを作成、enemysに追加(append)します。

        #敵の更新部分
        for e in self.enemys:
            e.update()
            if e.t == 1:
                if e.x > 160:
                    self.enemys.remove(e)
            elif e.t == 2:
                if e.x < -16:
                    self.enemys.remove(e)

敵の更新部分です。Appクラスのupdate部分に追記します。enemysの中にはEnemyインスタンスが入っていますので、これをforで取り出して、それぞれのupdateを呼んでやります。こうするとEnemyクラスで作ったupdateが呼び出されて、更新は完了です。
これだけでも良いのですが、画面外に行った敵は削除しないと永遠に存在することになり、アプリケーションが圧迫されます。右向きだったらx座標が160を超えたら、左向きの場合は-16より小さくなったら削除(remove)しておきましょう。

        #敵の描画
        for e in self.enemys:
            e.draw()

敵の描画部分です。Appクラスのdraw部分に追記します。こちらはenemysをforで回してEnemyクラスのdrawを呼び出すだけです。

▲ここまでを実行してみたところ。
だんだん形になってきました。

さあ、あとちょっとです。敵の弾を作っていきます。

プレイヤーの弾をに使ったBulletクラスがちょっと改造すれば敵にも流用できそうです。以下のように書き換えます。

#弾クラス
class Bullet:
    def __init__(self, x, y, t):
        #ここでオブジェクト作成時の処理をします   
        self.x = x
        self.y = y
        self.w = 3
        self.h = 3
        self.speed = 3
        self.t = t

    def update(self):
        #各オブジェクトが毎フレーム行う更新処理です
        if self.t == 1:
            self.y -= self.speed
        elif self.t == 2:
            self.y += self.speed

    def draw(self):
        #各オブジェクトが毎フレーム行う描画処理です
        pyxel.rect(self.x, self.y, self.w, self.h, 13)
        pyxel.rectb(self.x, self.y, self.w, self.h, 0)

新たにtというプロパティを設定しました。プレイヤーの弾の場合は1、敵の弾の場合は2が入るようにします。update部分にも手を入れ、tの値によって上に進むか下に進むか分岐させました。

        #攻撃部分
        if pyxel.btnp(pyxel.KEY_SPACE):
            #斬弾がある場合は発射
            if self.magazine > 0:
                self.magazine -= 1
                self.bullets.append(Bullet(self.player_pos[0] + 8, self.player_pos[1], 1))

Appクラスのupdate部分にあるプレイヤーの攻撃ロジックに手を入れます。新たに設定したBulletクラスのtに1を入れる形でインスタンスを作ります。

        #弾の更新部分#########################
        for b in self.bullets:
            b.update()
            if b.y < 0 or b.y > 250:
                self.bullets.remove(b)

弾の更新部分では、新たに下に動く弾が追加されたので、これに合わせてb.y > 250の条件を付けました。これで、画面の下に行った弾が削除されるようになりました。

        #敵の更新部分
        for e in self.enemys:
            if e.update() == True:
                self.bullets.append(Bullet(e.x + 8, e.y, 2))

            if e.t == 1:
                if e.x > 160:
                    self.enemys.remove(e)
            elif e.t == 2:
                if e.x < -16:
                    self.enemys.remove(e)

敵の更新部分ではEnemyクラスのupdateがTrueを返したときにBulletクラスのインスタンスを作成するようにしました。これで敵の攻撃が行われるようになります。
Enemyクラスのupdateには以下のような追記をしています。

    def update(self):
        #各オブジェクトが毎フレーム行う更新処理です
        if self.t == 1:
            self.x += self.speed
        elif self.t == 2:
            self.x -= self.speed

        if pyxel.frame_count % pyxel.rndi(30, 90) == 0:
            return True

pyxel.frame_count % pyxel.rndi(30, 90) == 0の条件に入った際にTrueを返します。これは敵が攻撃する間隔です。

▲ここまでを動かしたところ。
敵が弾を撃ってくるようになりました。

いよいよ大詰め、当たり判定を作ります。

当たり判定を作るという事は、状況によってはゲームオーバーになる場合があるという事。まず、その管理用変数を作りましょう。

        self.game_over = False

Appクラスのinitにgame_overを作り、Falseにしておきます。
プレイヤーが敵の弾に当たったら、これをTrueにします。

        #弾の更新部分#########################
        for b in self.bullets:
            self. game_over = b.update(self.player_pos, self.enemys, self.game_over)
            if b.y < 0 or b.y > 250:
                self.bullets.remove(b)

Appクラスupdateの弾の更新部分を修正します。Bulletクラスのupdateにself.player_pos, self.enemys, self.game_overを渡して当たり判定を行わせます。また、戻り値をgame_overに入れるようにします。

#弾クラス
class Bullet:
    def __init__(self, x, y, t):
        #ここでオブジェクト作成時の処理をします   
        self.x = x
        self.y = y
        self.w = 3
        self.h = 3
        self.speed = 3
        self.t = t

    def update(self, p, es, g):
        #各オブジェクトが毎フレーム行う更新処理です
        if self.t == 1:
            self.y -= self.speed
            for e in es:
                if self.x > e.x and self.x < e.x + 16:
                    if self.y > e.y and self.y < e.y + 16:
                        es.remove(e)

        elif self.t == 2:
            self.y += self.speed           
            if self.x > p[0] and self.x < p[0] + 16:
                if self.y > p[1] and self.y < p[1] + 16:
                    g = True 
                    return g   
        return g         

Bulletクラスのupdateを直します。
まず、if self.t == 1:つまりプレイヤーの弾の場合は、敵との当たり判定を付けます。
引数として受け取ったenemys(es)をforで回します。その後、if self.x > e.x and self.x < e.x + 16:で弾のx座標が、if self.y > e.y and self.y < e.y + 16:で弾のy座標が敵と重なっているかを判定します。どちらもTrueの場合には下のネストに入り、es.remove(e)で該当の敵を削除します。
次に、elif self.t == 2:敵の弾の場合です。今度はプレイヤー座標と弾の座標を比較しますが、形はt == 1の時と同じです。x座標とy座標を比較し、どちらもTrueになったら引数として受け取ったgame_over(g)をTrueにします。最後にgをreturnして終わりです。
また、弾が当たらずネストに入らなかった場合に備えてif分の外にもreturn gを追記しておきましょう。

        #ゲームオーバー表記
        if self.game_over == True:
            pyxel.text(20, 50, "GAME OVER!!" , 0)
            pyxel.text(21, 51, "GAME OVER!!" , 7)
            pyxel.text(20, 70, "PLEASE HIT SPACE-KEY" , 0)
            pyxel.text(21, 71, "PLEASE HIT SPACE-KEY" , 7)

ゲームオーバーになった場合はそう分かるようにAppクラスのdrawに文字表記の文を追記しておきます。

        #コントロール部分###################
        #移動部分
        if self.game_over == True:
            if pyxel.btn(pyxel.KEY_SPACE):
                self.enemys = []
                self.bullets = []
                self.game_over = False
        else:
            if pyxel.btn(pyxel.KEY_RIGHT):
                if self.player_pos[0] + self.speed < 160 - 16:
                    self.player_pos[0] += self.speed
                    self.moving_flag = True
            
            elif pyxel.btn(pyxel.KEY_LEFT):
                if self.player_pos[0] - self.speed > 0:
                    self.player_pos[0] -= self.speed
                    self.moving_flag = True

            else:
                self.moving_flag = False

            #攻撃部分
            if pyxel.btnp(pyxel.KEY_SPACE):
                #残弾がある場合は発射
                if self.magazine > 0:
                    self.magazine -= 1
                    self.bullets.append(Bullet(self.player_pos[0] + 8, self.player_pos[1], 1))

また、Appクラスのupdateではコントロール部分をgame_overの値によって切り替えるようにします。ゲームオーバー時にはスペースキーでリトライができるようにしました。

さあ完成です。
すべてつなげたソースは長いですが以下の通り。また、今回使った画像ファイルも載せています。

import pyxel


class App:
    def __init__(self):
        #ここで起動時の処理をします                                
        pyxel.init(160, 250)        
        pyxel.load('./sample03.pyxres')    
        self.player_pos = [5, 200]         
        self.speed = 2
        self.moving_flag = False
        self.bullets = []
        self.magazine = 5
        self.magazine_count = 0
        self.enemys = []
        self.game_over = False
        pyxel.run(self.update, self.draw)

    def update(self):
        #ここで毎フレームの更新作業をします
        #敵の作成部分
        if pyxel.frame_count % pyxel.rndi(120, 240) == 0:
            new_enemy_count = pyxel.rndi(1, 3)
            for nw in range(new_enemy_count):
                self.enemys.append(Enemy())

        #弾の装填部分######################
        self.magazine_count += 1
        if self.magazine_count > 30:
            self.magazine_count = 0
            if self.magazine < 5:
                self.magazine += 1


        #コントロール部分###################
        #移動部分
        if self.game_over == True:
            if pyxel.btn(pyxel.KEY_SPACE):
                self.enemys = []
                self.bullets = []
                self.game_over = False
        else:
            if pyxel.btn(pyxel.KEY_RIGHT):
                if self.player_pos[0] + self.speed < 160 - 16:
                    self.player_pos[0] += self.speed
                    self.moving_flag = True
            
            elif pyxel.btn(pyxel.KEY_LEFT):
                if self.player_pos[0] - self.speed > 0:
                    self.player_pos[0] -= self.speed
                    self.moving_flag = True

            else:
                self.moving_flag = False

            #攻撃部分
            if pyxel.btnp(pyxel.KEY_SPACE):
                #残弾がある場合は発射
                if self.magazine > 0:
                    self.magazine -= 1
                    self.bullets.append(Bullet(self.player_pos[0] + 8, self.player_pos[1], 1))
 
        #弾の更新部分#########################
        for b in self.bullets:
            self. game_over = b.update(self.player_pos, self.enemys, self.game_over)
            if b.y < 0 or b.y > 250:
                self.bullets.remove(b)
        #敵の更新部分
        for e in self.enemys:
            if e.update() == True:
                self.bullets.append(Bullet(e.x + 8, e.y, 2))

            if e.t == 1:
                if e.x > 160:
                    self.enemys.remove(e)
            elif e.t == 2:
                if e.x < -16:
                    self.enemys.remove(e)


    def draw(self):
        #ここで毎フレームの描画作業をします
        pyxel.cls(1)        

        #プレイヤーの描画
        if self.moving_flag == True:
            pyxel.blt(self.player_pos[0], self.player_pos[1], 0, 16, 0, 16, 16, 14)
        else:
            pyxel.blt(self.player_pos[0], self.player_pos[1], 0, 0, 0, 16, 16, 14)

        #残弾表記部分の描画
        pyxel.rect(0, 228, 160, 22, 0)
        pyxel.rectb(0, 228, 160, 22, 7)
        for i in range(5):
            if i+1 > self.magazine:
                pyxel.blt(5 + 8*i, 232, 0, 0, 16, 8, 16, 14)
            else:
                pyxel.blt(5 + 8*i, 232, 0, 8, 16, 8, 16, 14)

        #プレイヤーの弾の描画
        for b in self.bullets:
            b.draw()
    
        #敵の描画
        for e in self.enemys:
            e.draw()

        #ゲームオーバー表記
        if self.game_over == True:
            pyxel.text(20, 50, "GAME OVER!!" , 0)
            pyxel.text(21, 51, "GAME OVER!!" , 7)
            pyxel.text(20, 70, "PLEASE HIT SPACE-KEY" , 0)
            pyxel.text(21, 71, "PLEASE HIT SPACE-KEY" , 7)



#弾クラス
class Bullet:
    def __init__(self, x, y, t):
        #ここでオブジェクト作成時の処理をします   
        self.x = x
        self.y = y
        self.w = 3
        self.h = 3
        self.speed = 3
        self.t = t

    def update(self, p, es, g):
        #各オブジェクトが毎フレーム行う更新処理です
        if self.t == 1:
            self.y -= self.speed
            for e in es:
                if self.x > e.x and self.x < e.x + 16:
                    if self.y > e.y and self.y < e.y + 16:
                        es.remove(e)

        elif self.t == 2:
            self.y += self.speed           
            if self.x > p[0] and self.x < p[0] + 16:
                if self.y > p[1] and self.y < p[1] + 16:
                    g = True 
                    return g   
        return g         



    def draw(self):
        #各オブジェクトが毎フレーム行う描画処理です
        pyxel.rect(self.x, self.y, self.w, self.h, 13)
        pyxel.rectb(self.x, self.y, self.w, self.h, 0)


#敵クラス
class Enemy:
    def __init__(self):
        #ここでオブジェクト作成時の処理をします   
        self.t = pyxel.rndi(1, 2)
        if self.t == 1:
            self.x = pyxel.rndi(-60, -16)            
        elif self.t == 2:
            self.x = pyxel.rndi(180, 240)
        self.y = pyxel.rndi(16, 150)        
        self.speed = pyxel.rndi(1, 2)                

    def update(self):
        #各オブジェクトが毎フレーム行う更新処理です
        if self.t == 1:
            self.x += self.speed
        elif self.t == 2:
            self.x -= self.speed

        if pyxel.frame_count % pyxel.rndi(30, 90) == 0:
            return True

    def draw(self):
        #各オブジェクトが毎フレーム行う描画処理です
        if self.t == 1:
            pyxel.blt(self.x, self.y, 0, 0, 48, 16, 16, 14)
        elif self.t == 2:
            pyxel.blt(self.x, self.y, 0, 0, 32, 16, 16, 14)

App()
▲弾に当たったら敵が消えるようになりました。
▲敵の弾に当たった場合はゲームオーバーになります。
スペースキーを押すとリトライできます。

本noteではここまでですが、ここから改善するとすれば何が良いでしょう。

弾が当たった時があまりに味気ないので爆発のようなエフェクトを付けても良いかもしれません。あるいは船がブクブクと沈む画像を付けても良いでしょう。背景も青一色ですがここに波のエフェクトが付けばぐっと雰囲気が出そうです。

結構長くなってしまい分かりにくい箇所もあったかと思いますが、ここまで読んでいただきありがとうございました。

※有料エリアですが、特に何もありません。設定しているだけですので、お気に召しましたら購入いただけると嬉しいです。

ここから先は

28字

¥ 100

この記事が参加している募集

ここまで読んでいただきありがとうございます!