pyxelで雪合戦ゲームを作る
このnoteではpythonでレトロゲームを作れるpyxelというゲームエンジンで雪合戦ゲームを作成してみます。基本的な構造はいわゆるシューティングゲームと同じ(敵の攻撃をよけながら敵に弾を当てる)です。今回自分で設定した目的は、「キーボードでプレイヤーを操作する」、「敵にも弾を撃たせる」、「リトライができるようにする」の3つです。
部分に分けてコードを載せていきます。
from random import randint
import pyxel
WINDOW_W = 255
WINDOW_H = 255
class APP:
def __init__(self):
self.player = Player()
self.hit_x = 0
self.hit_y = 0
self.shots = []
self.e_shots = []
self.f_shots = []
self.game_over = False
self.game_end = False
self.game_start = False
self.enemys = []
self.friends = []
self.shot_flug = False
for i in range(8):
new_enemy = Enemy1()
self.enemys.append(new_enemy)
for i in range(8):
new_enemy = Enemy2()
self.enemys.append(new_enemy)
for i in range(7):
new_friend = Friend1()
self.friends.append(new_friend)
for i in range(8):
new_friend = Friend2()
self.friends.append(new_friend)
pyxel.init(WINDOW_W, WINDOW_H, caption="雪合戦")
# 作成したドット絵やタイルマップの情報を読み込む
pyxel.load("C:/pyxel/image/snow.pyxel")
pyxel.mouse(False)
pyxel.run(self.update, self.draw)
まずは__init__関数です。変数の定義が主になります。敵チームと味方チームはリストで管理します。rangeを決めて、指定回数ループさせることで作成します。pyxel.runでゲーム開始です。
def init(self): #リトライ時の初期化関数
self.player = Player()
self.hit_x = 0
self.hit_y = 0
self.shots = []
self.e_shots = []
self.f_shots = []
self.game_over = False
self.game_end = False
self.game_start = True
self.enemys = []
self.friends = []
self.shot_flug = False
for i in range(8):
new_enemy = Enemy1()
self.enemys.append(new_enemy)
for i in range(8):
new_enemy = Enemy2()
self.enemys.append(new_enemy)
for i in range(7):
new_friend = Friend1()
self.friends.append(new_friend)
for i in range(8):
new_friend = Friend2()
self.friends.append(new_friend)
今回はリトライ機能も付けるのでリトライ用のinit関数を用意しました。内容は起動時の__init__関数とほぼ同じですが、ウィンドウの作成などの不要な部分は削っています。ここでリトライ時に変数の初期化をします。
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
if pyxel.btnp(pyxel.KEY_R):
self.init()
if pyxel.btnp(pyxel.KEY_S):
self.game_start = True
self.init()
#プレイヤーのアップデート
if self.game_over == False:
#マウスで動かす場合
#if ((pyxel.mouse_x > 0) and (pyxel.mouse_x < WINDOW_W) and
#(pyxel.mouse_y > 0) and (pyxel.mouse_y > WINDOW_H / 2)):
#self.player.update(pyxel.mouse_x, pyxel.mouse_y)
#キーで動かす場合
#上
if (self.player.player_y > WINDOW_H / 2):
if pyxel.btn(pyxel.KEY_UP):
self.player.update(self.player.player_x,
self.player.player_y-2)
#下
if (self.player.player_y < WINDOW_H - 5):
if pyxel.btn(pyxel.KEY_DOWN):
self.player.update(self.player.player_x,
self.player.player_y+2)
#右
if (self.player.player_x < WINDOW_W - 5):
if pyxel.btn(pyxel.KEY_RIGHT):
self.player.update(self.player.player_x+2,
self.player.player_y)
#左
if (self.player.player_x > 0):
if pyxel.btn(pyxel.KEY_LEFT):
self.player.update(self.player.player_x-2,
self.player.player_y)
update関数に入ります。今回はQで終了、Rでリトライ、最初はSでスタートにしてみました。スタート時の操作はリトライと変わらないのですが、起動したとたんにゲームが始まるのもいかがなものかということで付けてみました。プレイヤーについては各キーに応じて上下左右に移動させています。移動制限もかけていますが、プレーヤーの座標が見た目の中心と一致していないので、少し小細工をしています。今回のプレイヤーは8×8でしたが、もっと大きくなる場合は工夫しないと不自然になりそうです(体の一部がウィンドウからはみ出してしまう)。
#味方のアップデート
friend_count = len(self.friends)
if pyxel.frame_count % 2 == 0:
for f in range (friend_count):
friend_vec1 = randint(1, 4)
friend_vec2 = friend_vec1 % 5
if friend_vec2 == 1: #右に移動
if (self.friends[f].fri_x < WINDOW_W - 5):
self.friends[f].fri_x = self.friends[f].fri_x + 2
elif friend_vec2 == 2: #左に移動
if (self.friends[f].fri_x > 0):
self.friends[f].fri_x = self.friends[f].fri_x - 2
elif friend_vec2 == 3: #雪玉を投げる1
if pyxel.frame_count % 50 == 0:
f_new_shot = Shot()
f_new_shot.update(self.friends[f].fri_x,
self.friends[f].fri_y, 7)
self.f_shots.append(f_new_shot)
elif friend_vec2 == 4: #雪玉を投げる2
if pyxel.frame_count % 80 == 0:
f_new_shot = Shot()
f_new_shot.update(self.friends[f].fri_x,
self.friends[f].fri_y, 7)
self.f_shots.append(f_new_shot)
#敵のアップデート
enemy_count = len(self.enemys)
if pyxel.frame_count % 2 == 0:
for e in range (enemy_count):
enemy_vec1 = randint(1, 4)
enemy_vec2 = enemy_vec1 % 5
if enemy_vec2 == 1: #右に移動
if (self.enemys[e].ene_x < WINDOW_W - 5):
self.enemys[e].ene_x = self.enemys[e].ene_x + 3
elif enemy_vec2 == 2: #左に移動
if (self.enemys[e].ene_x > 0):
self.enemys[e].ene_x = self.enemys[e].ene_x - 3
elif enemy_vec2 == 3: #雪玉を投げる1
if pyxel.frame_count % 25 == 0:
e_new_shot = Shot()
e_new_shot.update(self.enemys[e].ene_x,
self.enemys[e].ene_y, 7)
self.e_shots.append(e_new_shot)
elif enemy_vec2 == 4: #雪玉を投げる2
if pyxel.frame_count % 40 == 0:
e_new_shot = Shot()
e_new_shot.update(self.enemys[e].ene_x,
self.enemys[e].ene_y, 7)
self.e_shots.append(e_new_shot)
update関数の続き、味方と敵のアップデートです。基本的にはどちらも同じです。行動は左右の移動と雪玉を投げるだけですが、frame_countのみで制御するとタイミングが一緒になってしまい面白みに欠けます。そこで、randintと組み合わせてできるだけ行動がばらけるようにしました。雪玉を投げるタイミングはさらにパターンを分けています。構造は同じですが、敵の方が攻撃頻度は多くなるようにしてみました。この辺りは数値をいじるだけですぐ反映されるので、いろいろ試してみました。
#自雪玉のアップデート
#SPACEが押された際に発射する
if self.game_over == False: #連射防止機能
if pyxel.btn(pyxel.KEY_SPACE) == False:
self.shot_flug = False
if self.game_over == False:
if self.shot_flug == False:
if pyxel.btn(pyxel.KEY_SPACE) == True:
self.shot_flug = True
if len(self.shots) < 5:
new_shot = Shot()
new_shot.update(self.player.player_x,
self.player.player_y, 7)
self.shots.append(new_shot)
#自雪玉当たり判定
shot_count = len(self.shots)
for j in range (shot_count):
if self.shots[j].pos_y > 1:
self.shots[j].pos_y = self.shots[j].pos_y - 4
#敵との当たり判定
enemy_count = len(self.enemys)
for e in range(enemy_count):
if ((self.shots[j].pos_x >= self.enemys[e].ene_x - 7) and
(self.shots[j].pos_x <= self.enemys[e].ene_x + 2) and
(self.shots[j].pos_y <= self.enemys[e].ene_y) and
(self.shots[j].pos_y >= self.enemys[e].ene_y - 8)):
#当たったら敵と弾を削除
del self.enemys[e]
del self.shots[j]
break
else:
continue
break
else:
del self.shots[j]
break
#味方雪玉当たり判定
shot_count = len(self.f_shots)
for j in range (shot_count):
if self.f_shots[j].pos_y > 1:
self.f_shots[j].pos_y = self.f_shots[j].pos_y - 4
#敵との当たり判定
enemy_count = len(self.enemys)
for e in range(enemy_count):
if ((self.f_shots[j].pos_x >= self.enemys[e].ene_x - 7) and
(self.f_shots[j].pos_x <= self.enemys[e].ene_x + 2) and
(self.f_shots[j].pos_y <= self.enemys[e].ene_y) and
(self.f_shots[j].pos_y >= self.enemys[e].ene_y - 8)):
#当たったら敵と弾を削除
del self.enemys[e]
del self.f_shots[j]
break
else:
continue
break
else:
del self.f_shots[j]
break
#敵雪玉当たり判定
shot_count = len(self.e_shots)
for j in range (shot_count):
if self.e_shots[j].pos_y < 250:
self.e_shots[j].pos_y = self.e_shots[j].pos_y + 4
#プレイヤーとの当たり判定
if ((self.e_shots[j].pos_x >= self.player.player_x - 7) and
(self.e_shots[j].pos_x <= self.player.player_x + 2) and
(self.e_shots[j].pos_y <= self.player.player_y) and
(self.e_shots[j].pos_y >= self.player.player_y - 8)):
#当たったらゲームオーバー
self.game_over = True
self.hit_x = self.player.player_x
self.hit_y = self.player.player_y
del self.e_shots[j]
break
#味方との当たり判定
friend_count = len(self.friends)
for f in range(friend_count):
if ((self.e_shots[j].pos_x >= self.friends[f].fri_x-7) and
(self.e_shots[j].pos_x <= self.friends[f].fri_x+2) and
(self.e_shots[j].pos_y <= self.friends[f].fri_y) and
(self.e_shots[j].pos_y >= self.friends[f].fri_y-8)):
#当たったら味方と弾を削除
del self.friends[f]
del self.e_shots[j]
break
else:
continue
break
else:
del self.e_shots[j]
break
#敵がいなくなったらクリア
enemy_count = len(self.enemys)
if enemy_count < 1:
self.game_end = True
雪玉関連です。今回はキー押っぱの連射をなくしたかったので、いろいろやってたらごちゃつきました…。とりあえず欲しかった動きにはなったのでよしとします。雪玉の当たり判定は各雪玉に対してそれぞれの敵と当たっているかをループ文で判定しています。当たった場合は、敵(味方)と雪玉を削除していますが、この時にbreakでループ文を抜けないと、エラーになってしまいます。おそらくは既に削除されたオブジェクトを参照しようとしているためだと思います。この2重ループの挙動は毎回よくわからなくなるので、一度しっかりまとめておきたいと思っています。
def draw(self):
pyxel.cls(0)
pyxel.rect(0, 124, 255, 125, 7)
#ゲームオーバー
if self.game_over:
pyxel.text(105, 75, "Game Over!", pyxel.frame_count % 16)
pyxel.text(105, 85, "R = Retry", pyxel.frame_count % 16)
pyxel.text(105, 95, "Q = Quit", pyxel.frame_count % 16)
#ゲームクリア
if self.game_end:
pyxel.text(105, 75, "Completed!", pyxel.frame_count % 16)
pyxel.text(105, 85, "R = Retry", pyxel.frame_count % 16)
pyxel.text(105, 95, "Q = Quit", pyxel.frame_count % 16)
#プレイヤーの描写
pyxel.blt(self.player.player_x, self.player.player_y, 0, 0, 0,
8, 8, 5)
#ヒット描画
if self.game_over == True:
pyxel.blt(self.hit_x, self.hit_y, 0, 16, 0, 8, 8, 5)
#弾の描画
for i in self.shots: #自分
pyxel.rect(i.pos_x+6, i.pos_y-0.5,
i.pos_x+6, i.pos_y+0.5, i.color)
for i in self.f_shots: #味方
pyxel.rect(i.pos_x+6, i.pos_y-0.5,
i.pos_x+6, i.pos_y+0.5, i.color)
for i in self.e_shots: #敵
pyxel.rect(i.pos_x+6, i.pos_y-0.5,
i.pos_x+6, i.pos_y+0.5, i.color)
#味方の描画
for i in self.friends:
pyxel.blt(i.fri_x, i.fri_y, 0, 0, 0, 8, 8, 5)
#敵の描画
for i in self.enemys:
pyxel.blt(i.ene_x, i.ene_y, 0, 0, 8, 8, 8, 5)
#ゲームスタートしていない場合は黒で上書き
if self.game_start == False:
pyxel.cls(0)
pyxel.text(105, 75, "Yukigassen", pyxel.frame_count % 16)
pyxel.text(105, 85, "S = Start", pyxel.frame_count % 16)
pyxel.text(105, 95, "Q = Quit", pyxel.frame_count % 16)
draw関数です。update関数でいじった座標に、どんどん描画していきます。ゲームがスタートしていない場合はすべての描画が終わった後で、ウィンドウを黒で塗りつぶして、開始前の状態としています。厳密には裏でゲームは始まっているんですけど、見た目には分かりません。ゲームスタートのフラグでどこまで制御するのかは正直よくわからないままです…。一番楽に見た目がそれっぽくなるので今回はこの方法にしました。
class Player:
def __init__(self):
self.player_x = 125
self.player_y = 235
def update(self, x, y):
self.player_x = x
self.player_y = y
class Shot:
def __init__(self):
self.speed = 3
self.pos_x = 0
self.pos_y = 0
self.color = 7 # 0~15
def update(self, x, y, color):
self.pos_x = x
self.pos_y = y
self.color = color
#味方後列
class Friend1:
def __init__(self):
self.fri_x = randint(10, 240)
self.fri_y = 215
def update(self, x, y):
self.fri_x = x
self.fri_y = y
#味方前列
class Friend2:
def __init__(self):
self.fri_x = randint(10, 240)
self.fri_y = 165
def update(self, x, y):
self.fri_x = x
self.fri_y = y
#敵後列
class Enemy1:
def __init__(self):
self.ene_x = randint(10, 240)
self.ene_y = 15
def update(self, x, y):
self.ene_x = x
self.ene_y = y
#敵前列
class Enemy2:
def __init__(self):
self.ene_x = randint(10, 240)
self.ene_y = 65
def update(self, x, y):
self.ene_x = x
self.ene_y = y
APP()
オブジェクトのクラスです。座標情報を扱うのが主です。作ったはいいけど結局使わなかった機能がありますね…。この辺りを改造して雪玉のスピードを可変にしても面白そうです。
こんなこんなで完成です。見た目は大分シンプルになっちゃいましたけど、やりたかったことができたので満足です。.pyxelファイルも参考までにあげておきます。誰かの参考になれば。
pyxel楽しくておすすめです。何より、コードがあまり複雑にならないので、ずぼらな自分でもとりあえず動く形に持っていけています。次は別のジャンルのゲームにも挑戦してみたいと思います。
ここまで読んでいただきありがとうございます!