見出し画像

pyxel 1.2を使ってゲームを作る(1)

pyxelのバージョンが上がってからあまり触っていませんでしたが、実行形式での保存もできるようになったとのことで、もう一度チャレンジしてみます。

今回の目標は「簡単なシューティングゲームを作って.exe形式にする」までを行いたいと思います。サブ目標として、「ゲームの拡張性を確保する」、具体的には「敵を簡単に追加できるようにする」というところに据えてやっていきたいと思います。

from random import randint
import pyxel

WINDOW_H = 120
WINDOW_W = 160
PIC_H = 16
PIC_W = 16

class APP:
 def __init__(self):
     self.game_start = False
     self.game_over = False
     self.game_end = False
     self.boss_flug = False
     self.boss_scr = 0
     self.boss_count = 1
     self.boss_color = 0
     self.score = 0
     self.shots = []
     self.enemys = []
     self.bombs = []
     self.p_ship = Ship()
     
     pyxel.init(WINDOW_W, WINDOW_H, caption="Pyxel Invaders")
     # ドット絵を読み込む
     pyxel.load("C:/pyxel/image/inve.pyxres")
     
     pyxel.mouse(False)
     
     pyxel.run(self.update, self.draw)
    
 def update(self):
     #システムコントロール
     if pyxel.btnp(pyxel.KEY_Q):
         pyxel.quit()
     if pyxel.btnp(pyxel.KEY_R):
         self.game_start = False
     if pyxel.btnp(pyxel.KEY_S):
         self.game_start = True
         self.retry()
         
     #自機の更新
     if self.game_over == False:
         self.ship_move()

                 
     if self.game_end == False:
         self.hit_chk()
         self.ene_move()
     
     #画面の爆発が3以上になったら古いものから消していく
     if len(self.bombs) > 3:
         del self.bombs[0]  
                 
             
 def draw(self):
     pyxel.cls(0)
     
     #得点描写
     pyxel.text(1, 2, "SCORE:" + str(self.score), 7)
     
     #宇宙船の描画
     if self.game_over == False:
         pyxel.blt(self.p_ship.ship_x, self.p_ship.ship_y, 0, 32, 0, 
                   -PIC_W, PIC_H, 6)
     else:
         pyxel.blt(self.p_ship.ship_x, self.p_ship.ship_y, 0, 64, 
                             0,-PIC_W, PIC_H, 6)      
     
     #弾の描画
     for i in self.shots:
         pyxel.rect(i.pos_x+7, i.pos_y-3,
                    2, 2, 12)
         
      #敵の描画
     for i in self.enemys:
         if self.boss_flug == False:
             if i.ene_f == 0:
                 pyxel.blt(i.ene_x, i.ene_y, 0, 0, i.ene_c * 32, 
                           -PIC_W, PIC_H, 6)
             else:
                 pyxel.blt(i.ene_x, i.ene_y, 0, 16, i.ene_c * 32, 
                           -PIC_W, PIC_H, 6)
         else:
              pyxel.blt(i.ene_x, i.ene_y, 0, 48, 16 * self.boss_color,
                        16, 16, 6)
         
     #ゲームスタート画面
     if self.game_start == False:
          pyxel.cls(0)
          pyxel.text(20, 30, "PYXEL INVADERS", 10)
          pyxel.text(100, 60, "READY? ", pyxel.frame_count % 16)
          pyxel.text(100, 70, "S = START ", pyxel.frame_count % 16)
          pyxel.text(100, 80, "Q = QUIT ", pyxel.frame_count % 16)
       
       
 def retry(self): #リトライ時のリセット関数
     self.game_over = False
     self.game_end = False
     self.boss_flug = False
     self.boss_count = 1
     self.boss_color = 0
     self.score = 0
     self.shots = []
     self.enemys = []
     self.bombs = []
     self.p_ship = Ship()
     
 def ship_move(self): #自機を動かす関数
     if pyxel.btn(pyxel.KEY_RIGHT):
         self.p_ship.update(self.p_ship.ship_x + 2, self.p_ship.ship_y)
     if pyxel.btn(pyxel.KEY_LEFT):
         self.p_ship.update(self.p_ship.ship_x - 2, self.p_ship.ship_y)
     #SPACEが押された際に発射する
     if pyxel.btnp(pyxel.KEY_SPACE, 5, 15):
         if len(self.shots) < 11:
             new_shot = Shot()
             new_shot.update(self.p_ship.ship_x, self.p_ship.ship_y, 8)
             self.shots.append(new_shot)
                 
 def ene_move(self): #敵を動かす関数
     #通常時
     if self.boss_flug == False:
         if pyxel.frame_count % 20 == 0:
             new_enemy = Enemy()
             self.enemys.append(new_enemy)
     #ボス攻撃
     else:
         atk = 20 - self.boss_count
         if pyxel.frame_count % atk == 0:
             new_enemy = Enemy()
             new_enemy.ene_x = randint(self.boss.boss_x, 
                                        self.boss.boss_x + 40)
             new_enemy.ene_y = 20
             self.enemys.append(new_enemy)
     
     enemy_count = len(self.enemys)
     for e in range (enemy_count):
         enemy_vec1 = randint(0, 7)
         enemy_vec2 = enemy_vec1 % 2
         if self.enemys[e].ene_y < 115:
             self.enemys[e].ene_y = self.enemys[e].ene_y + 1.0
             if pyxel.frame_count % 50 == 0:
                 if self.boss_flug == False:
                     if enemy_vec2 > 0:
                         self.enemys[e].ene_x = self.enemys[e].ene_x + 4
                         if self.enemys[e].ene_f == 0:
                             self.enemys[e].ene_f = 1
                         else:
                             self.enemys[e].ene_f = 0
                     else:
                         self.enemys[e].ene_x = self.enemys[e].ene_x - 4
                         if self.enemys[e].ene_f == 0:
                             self.enemys[e].ene_f = 1
                         else:
                             self.enemys[e].ene_f = 0
                 else:
                     continue
         else:
             del self.enemys[e]
             break
     
     
 def hit_chk(self): #当たり判定関数
     shot_count = len(self.shots)
     #上限を超えた弾を削除
     for j in range (shot_count):
         if self.shots[j].pos_y > 10:
             self.shots[j].pos_y = self.shots[j].pos_y - 3
         else:
             del self.shots[j]
             break
       #当たり判定
         #敵と弾
         shot_hit = len(self.shots)
         if self.boss_flug == False:
             for h in range (shot_hit):
                 enemy_hit = len(self.enemys)
                 for e in range (enemy_hit):
                     if ((self.enemys[e].ene_x - 8 <= self.shots[h].pos_x 
                          <= self.enemys[e].ene_x + 8)and
                          (self.enemys[e].ene_y - 7 <= self.shots[h].pos_y <= 
                           self.enemys[e].ene_y + 15)):
                         #敵に当たったらその座標に爆発を乗せる
                         new_bomb = Bomb(self.enemys[e].ene_x, 
                                         self.enemys[e].ene_y)
                         self.bombs.append(new_bomb)
                         del self.enemys[e]
                         if self.boss_flug == False:
                             self.score = self.score + 100
                             break#敵に当たったらbreak
                     else:
                         continue
                     break#敵に当たったらbreak
     #敵と自機
     enemy_atk = len(self.enemys)
     for e in range (enemy_atk):
         if self.boss_flug == False:
             #4か所で接触を検知
             #1
             if (((self.enemys[e].ene_x + 3 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 3 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y <= self.p_ship.ship_y + 14))or
             #2
                  (self.enemys[e].ene_x + 12 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 12 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y <= self.p_ship.ship_y + 14)or
             #3
                  (self.enemys[e].ene_x + 3 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 3 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y + 6 >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y + 6 <= self.p_ship.ship_y + 14)or
             #4
                  ((self.enemys[e].ene_x + 12 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 12 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y + 6 >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y + 6 <= self.p_ship.ship_y + 14))):
                   self.game_over = True
         else:
             #2か所で接触を検知
             #1
             if (((self.enemys[e].ene_x + 8 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 8 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y <= self.p_ship.ship_y + 14))or
             #2
                  ((self.enemys[e].ene_x + 8 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 8 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y + 4 >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y + 4 <= self.p_ship.ship_y + 14))):
                   self.game_over = True

     
#オブジェクトのクラス  
class Ship:   
 def __init__(self):
     self.ship_x = 70
     self.ship_y = 105
 
 def update(self, x, y):
     self.ship_x = x
     self.ship_y = y
     
class Shot:
 def __init__(self):
     self.pos_x = 0
     self.pos_y = 0
     self.color = 8 # 0~15
 def update(self, x, y, color):
     self.pos_x = x
     self.pos_y = y
     self.color = color
     
class Enemy:
 def __init__(self):
     self.ene_x = randint(20, 125)
     self.ene_y = 5
     self.ene_f = 0
     self.ene_c = randint(0, 2)
 def update(self, x, y):
     self.ene_x = x
     self.ene_y = y
     
class Bomb:
 def __init__(self, x, y):
     self.bomb_x = x
     self.bomb_y = y       
     
                 
APP()

少し説明を加えていくと、先頭の

from random import randint
import pyxel

WINDOW_H = 120
WINDOW_W = 160
PIC_H = 16
PIC_W = 16

class APP:
 def __init__(self):
     self.game_start = False
     self.game_over = False
     self.game_end = False
     self.boss_flug = False
     self.score = 0
     self.shots = []
     self.enemys = []
     self.bombs = []
     self.p_ship = Ship()
     
     pyxel.init(WINDOW_W, WINDOW_H, caption="Pyxel Invaders")
     # ドット絵を読み込む
     pyxel.load("C:/pyxel/image/inve.pyxres")
     
     pyxel.mouse(False)
     
     pyxel.run(self.update, self.draw)

ここまでは今回利用するもののinport文や変数が並んでいます。WINDOW_H、Wはウィンドウサイズ、PIC_H、Wはキャラクターの基本の大きさです。
game_start(ゲームが始まっているか)、game_over(ゲームオーバーかどうか)、game_end(ゲームクリアかどうか)の三つの状態と、ボス用のboss_flug、計4つのフラグでゲームを管理していきたいと思います。あとは、弾、敵、爆発エフェクトの配列と、自機のインスタンスを作成しています。
pyxel.run(self.update, self.draw)でゲームが動作します。座標の更新と描画を繰り返しながら、ゲームは進んでいきます。

 def update(self):
     #システムコントロール
     if pyxel.btnp(pyxel.KEY_Q):
         pyxel.quit()
     if pyxel.btnp(pyxel.KEY_R):
         self.game_start = False
     if pyxel.btnp(pyxel.KEY_S):
         self.game_start = True
         self.retry()
         
     #自機の更新
     if self.game_over == False:
         self.ship_move()

                 
     if self.game_end == False:
         self.hit_chk()
         self.ene_move()
     
     #画面の爆発が3以上になったら古いものから消していく
     if len(self.bombs) > 3:
         del self.bombs[0]  

今回はなるべく関数にできるところは関数にして、後からでも手を入れられるように頑張ってみます。
まずは各ボタンに対応した動作を設定します。Qで終了、Rでリトライ、Sはスタートです。
次にgame_overがfalseの場合、つまりゲームオーバーではない場合には、ship_moveの関数を呼び出して自機を移動させます。
game_endがfalseの場合はゲームがクリアされていない状態ですので、当たり判定チェックのhit_chkと敵を動かすene_move関数を呼び出します。
その下の部分は爆発エフェクトが溜まりすぎないように、3つ以上になったら消している部分です。

 def draw(self):
     pyxel.cls(0)
     
     #得点描写
     pyxel.text(1, 2, "SCORE:" + str(self.score), 7)
     
     #宇宙船の描画
     if self.game_over == False:
         pyxel.blt(self.p_ship.ship_x, self.p_ship.ship_y, 0, 32, 0, 
                   -PIC_W, PIC_H, 6)
     else:
         pyxel.blt(self.p_ship.ship_x, self.p_ship.ship_y, 0, 64, 
                             0,-PIC_W, PIC_H, 6)      
     
     #弾の描画
     for i in self.shots:
         pyxel.rect(i.pos_x+7, i.pos_y-3,
                    2, 2, 12)
         
      #敵の描画
     for i in self.enemys:
         if self.boss_flug == False:
             if i.ene_f == 0:
                 pyxel.blt(i.ene_x, i.ene_y, 0, 0, i.ene_c * 32, 
                           -PIC_W, PIC_H, 6)
             else:
                 pyxel.blt(i.ene_x, i.ene_y, 0, 16, i.ene_c * 32, 
                           -PIC_W, PIC_H, 6)
         else:
              pyxel.blt(i.ene_x, i.ene_y, 0, 48, 16 * self.boss_color,
                        16, 16, 6)
         
     #ゲームスタート画面
     if self.game_start == False:
          pyxel.cls(0)
          pyxel.text(20, 30, "PYXEL INVADERS", 10)
          pyxel.text(100, 60, "READY? ", pyxel.frame_count % 16)
          pyxel.text(100, 70, "S = START ", pyxel.frame_count % 16)
          pyxel.text(100, 80, "Q = QUIT ", pyxel.frame_count % 16)

次は描画部分です。基本的にはpyxel.bltでドット絵を配置しているだけです。弾の描画には方形をつくるpyxel.rectを使います。また、敵の描画部分では、ボスの攻撃時と動作を使いまわすためにself.boss_flugでの分岐をつけています。Falseが通常時です。通常時にもさらに分岐がありますが、これは敵をアニメーション風にするための記述でene_fによって画像を変えています。
game_startフラグがfalseの場合には最終的にpyxel.cls(0)で画面を真っ黒にして、それぞれの文字をその上から表示します。

 def retry(self): #リトライ時のリセット関数
     self.game_over = False
     self.game_end = False
     self.boss_flug = False
     self.boss_count = 1
     self.boss_color = 0
     self.score = 0
     self.shots = []
     self.enemys = []
     self.bombs = []
     self.p_ship = Ship()
     
 def ship_move(self): #自機を動かす関数
     if pyxel.btn(pyxel.KEY_RIGHT):
         self.p_ship.update(self.p_ship.ship_x + 2, self.p_ship.ship_y)
     if pyxel.btn(pyxel.KEY_LEFT):
         self.p_ship.update(self.p_ship.ship_x - 2, self.p_ship.ship_y)
     #SPACEが押された際に発射する
     if pyxel.btnp(pyxel.KEY_SPACE, 5, 15):
         if len(self.shots) < 11:
             new_shot = Shot()
             new_shot.update(self.p_ship.ship_x, self.p_ship.ship_y, 8)
             self.shots.append(new_shot)

それぞれの関数部分です。retryは変数の初期化なので特に説明はありません。
ship_moveでは自機の動作をいろいろ設定しています。KEY_RIGHTで右に、KEY_LEFTで左に動きます。KEY_SPACEで弾を撃ちますが同時に存在できる弾の数は <11 部分で制御しています。

 def ene_move(self): #敵を動かす関数
     #通常時
     if self.boss_flug == False:
         if pyxel.frame_count % 20 == 0:
             new_enemy = Enemy()
             self.enemys.append(new_enemy)
     #ボス攻撃
     else:
         atk = 20 - self.boss_count
         if pyxel.frame_count % atk == 0:
             new_enemy = Enemy()
             new_enemy.ene_x = randint(self.boss.boss_x, 
                                        self.boss.boss_x + 40)
             new_enemy.ene_y = 20
             self.enemys.append(new_enemy)
     
     enemy_count = len(self.enemys)
     for e in range (enemy_count):
         enemy_vec1 = randint(0, 7)
         enemy_vec2 = enemy_vec1 % 2
         if self.enemys[e].ene_y < 115:
             self.enemys[e].ene_y = self.enemys[e].ene_y + 1.0
             if pyxel.frame_count % 50 == 0:
                 if self.boss_flug == False:
                     if enemy_vec2 > 0:
                         self.enemys[e].ene_x = self.enemys[e].ene_x + 4
                         if self.enemys[e].ene_f == 0:
                             self.enemys[e].ene_f = 1
                         else:
                             self.enemys[e].ene_f = 0
                     else:
                         self.enemys[e].ene_x = self.enemys[e].ene_x - 4
                         if self.enemys[e].ene_f == 0:
                             self.enemys[e].ene_f = 1
                         else:
                             self.enemys[e].ene_f = 0
                 else:
                     continue
         else:
             del self.enemys[e]
             break

次は敵を動かすene_move関数です。今回は通常の敵の動きをボスの攻撃に流用するのでboss_flugで分岐させます。pyxel.frame_countは経過フレームで、これを任意の数字で割って割り切れた際に敵を追加します。pyxel.frame_count % 20だとたぶん20フレームごとに敵が追加されるはずです。
その後はrandintを使ってenemy_vec1、enemy_vec2を生成。通常時にはenemy_vec2の値を見てx方向への敵の動きを加えます。画面買いに出た敵については if self.enemys[e].ene_y < 115 の条件文を通して del self.enemys[e] で削除します。

 def hit_chk(self): #当たり判定関数
     shot_count = len(self.shots)
     #上限を超えた弾を削除
     for j in range (shot_count):
         if self.shots[j].pos_y > 10:
             self.shots[j].pos_y = self.shots[j].pos_y - 3
         else:
             del self.shots[j]
             break
       #当たり判定
         #敵と弾
         shot_hit = len(self.shots)
         if self.boss_flug == False:
             for h in range (shot_hit):
                 enemy_hit = len(self.enemys)
                 for e in range (enemy_hit):
                     if ((self.enemys[e].ene_x - 8 <= self.shots[h].pos_x 
                          <= self.enemys[e].ene_x + 8)and
                          (self.enemys[e].ene_y - 7 <= self.shots[h].pos_y <= 
                           self.enemys[e].ene_y + 15)):
                         #敵に当たったらその座標に爆発を乗せる
                         new_bomb = Bomb(self.enemys[e].ene_x, 
                                         self.enemys[e].ene_y)
                         self.bombs.append(new_bomb)
                         del self.enemys[e]
                         if self.boss_flug == False:
                             self.score = self.score + 100
                             break#敵に当たったらbreak
                     else:
                         continue
                     break#敵に当たったらbreak
     #敵と自機
     enemy_atk = len(self.enemys)
     for e in range (enemy_atk):
         if self.boss_flug == False:
             #4か所で接触を検知
             #1
             if (((self.enemys[e].ene_x + 3 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 3 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y <= self.p_ship.ship_y + 14))or
             #2
                  (self.enemys[e].ene_x + 12 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 12 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y <= self.p_ship.ship_y + 14)or
             #3
                  (self.enemys[e].ene_x + 3 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 3 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y + 6 >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y + 6 <= self.p_ship.ship_y + 14)or
             #4
                  ((self.enemys[e].ene_x + 12 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 12 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y + 6 >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y + 6 <= self.p_ship.ship_y + 14))):
                   self.game_over = True
         else:
             #2か所で接触を検知
             #1
             if (((self.enemys[e].ene_x + 8 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 8 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y <= self.p_ship.ship_y + 14))or
             #2
                  ((self.enemys[e].ene_x + 8 >= self.p_ship.ship_x + 2) and
                  (self.enemys[e].ene_x + 8 <= self.p_ship.ship_x + 14) and
                  (self.enemys[e].ene_y + 4 >= self.p_ship.ship_y) and
                  (self.enemys[e].ene_y + 4 <= self.p_ship.ship_y + 14))):
                   self.game_over = True

当たり判定のhit_chk関数です。ただ、最初の部分は自機の弾に関する記述です。if self.shots[j].pos_y > 10: の条件を通して、画面内の弾はself.shots[j].pos_y = self.shots[j].pos_y - 3 で上方向に進めて、それ以外は del self.shots[j] で削除します。その後は各弾に関して敵と当たったかを見ています。当たった場合は爆発エフェクトを乗せて、del self.enemys[e] で敵を削除、スコアを100点加算します。
敵と自機の当たり判定については、通常時は4点、ボス攻撃時には2点でチェックします。
この当たり判定の部分についてはごちゃごちゃで自分でも微妙な気がしているんですが、まぁとりあえずこのままで。敵と自機が当たった場合にはself.game_overをTrueにします。

#オブジェクトのクラス  
class Ship:   
 def __init__(self):
     self.ship_x = 70
     self.ship_y = 105
 
 def update(self, x, y):
     self.ship_x = x
     self.ship_y = y
     
class Shot:
 def __init__(self):
     self.pos_x = 0
     self.pos_y = 0
     self.color = 8 # 0~15
 def update(self, x, y, color):
     self.pos_x = x
     self.pos_y = y
     self.color = color
     
class Enemy:
 def __init__(self):
     self.ene_x = randint(20, 125)
     self.ene_y = 5
     self.ene_f = 0
     self.ene_c = randint(0, 2)
 def update(self, x, y):
     self.ene_x = x
     self.ene_y = y
     
class Bomb:
 def __init__(self, x, y):
     self.bomb_x = x
     self.bomb_y = y       
     
                 
APP()

最後はオブジェクトのクラスです。ここは特に何もないですね。どのオブジェクトも基本的にはxとyを持っていて、update()で値を更新したりとか、そんな感じです。App()でプログラムが動き始めます。

さて、ソースは出来たので、実行形式プログラムにしていきたいと思います。
実行環境はAnacondaなので、ターミナルを開き、以下のコマンドを打ち込みます。

pyxelpackager ファイルのフルパス

すると、.pyファイルと同じフォルダ内にあるdistフォルダの中に.exeのファイルができていました。
ポチポチすると、

画像1

動きました!
まだまだ物足りない感はありますが、とりあえず今回はこれで良しとします。

今回使った画像ファイルです。よろしければどうぞ↓


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