弾を発射するためには、いくつかやらなくちゃいけないことがあるけど、とりあえず弾を表示するだけのコードを書いてみよう
弾はシンプルに、pyxel の機能でできる四角形を使おう。 スペースキーが押されたときに、黄色い資格がキャラクターの上に表示されるようにしてみよう。
import pyxel
pyxel.init(120, 120)
pyxel.load("danmaku.pyxres")
character = {
"x": 120 / 2 - 16 / 2,
"y": 90,
"tama": []
}
enemy = {
"x": 120 / 2 - 16 / 2,
"y": 20,
"direction": 1
}
def update():
print(character["tama"])
enemy["x"] = enemy["x"] + enemy["direction"]
if enemy["x"] >= 120 - 16 or enemy["x"] <= 16:
enemy["direction"] = enemy["direction"] * -1
if pyxel.btn(pyxel.KEY_RIGHT) == True:
character["x"] = character["x"] + 1
elif pyxel.btn(pyxel.KEY_LEFT) == True:
character["x"] = character["x"] - 1
if pyxel.btnp(pyxel.KEY_SPACE) == True:
character["tama"].append([character["x"], character["y"] - 16])
def draw():
pyxel.cls(0)
pyxel.blt(character["x"], character["y"], 0, 0, 0, 16, 16, 0)
pyxel.blt(enemy["x"], enemy["y"], 0, 16, 0, 16, 16, 0)
for i in character["tama"]:
pyxel.blt(i[0], i[1], 0, 32, 0, 16, 16, 0)
pyxel.run(update, draw)

弾を動かすために、[tama]の中にある弾丸を、y方向に1づつ移動させよう。この処理をupdate関数の中に追加します
for i in character["tama"]:
i[1] = i[1] - 1
これで、ゲームっぽくなってきた!!

今ターミナルを見てみると、めちゃくちゃ文字がたくさん出ていて、何がなんだかって感じになってる

うん、キモい。 これは、update()関数の一番最初に
print(character["tama"])
が入っているからなんだけど。tamaリスト、つまり弾丸の座標のリストが、弾を打つたびに増えていってるから。
見て分かる通り、弾が画面外に出たあとも表示されているし、数字が変わっているね。表示場はうまくいってても、裏側では、もう画面上ん存在しない弾の処理を、プログラムがさせられている状態。
「うまくいってるんだからいいじゃん」って言ってこれを放置しておくと、例えばゲーム後半で処理が重くなって、ゲームの動作が不安定になったり、思わぬところでバグが発生したりして、大変なことになりかねない。
じゃあ、必要のない弾を消してしまおう。
弾を消すには、必要ない座標のリストをtamaリストから消してしまえばいいんだけど、どういう条件で弾を消すかを先に考えよう。
この2つを、順番に実装していこう。
敵に当たらずに画面外に出るケースを実装しよう。 画面外に出るということはどういうことか。y方向に移動しているわけだから、弾の座標が -16 になったら画面外に出る。そうなったら用済みなので、弾を消しちゃおう。 つまり、tamaリストの中から、y軸の数字が -16 以下になっているやつを探して、そいつを削除すればいいわけだ。
tamaリストは、pythonのデータ型の一つ、「リスト」というデータになっている。データを纏めて保存するのに便利な型なんだけど、いくつか特徴がある

今回の場合、右側(後ろの方)にデータを追加していってるので、一番古いデータは一番左になる。
pyxelは毎フレームごとに処理を実行するわけだから、例えば同じフレームで弾が2発打ててしまうことはあり得ない。つまり、毎回tamaリストにある全部の座標をチェックする必要はなくて、一番古いデータをチェックして、座標が-16以下になっていればそれを消す、って感じでOKなわけ。それを実装すると、こんな感じになる
# 一番古い弾のデータをチェック
if len(character["tama"]) and character["tama"][0][1] <= -16:
character["tama"].pop(0)
popていうのがリストから消す働きをしているんだけど、リストの詳しいことなどは今回は深く突っ込まないので、気になる人はリッキーたちに聞いてくれ♪
弾が消えてるかどうか、実行して確認してみよう

これでおk!
さて、この仕組みを考えよう
格ゲーとかシューティングゲームにおいて、このあたり判定がうまく調整されているかどうかは、そのゲームが神ゲーになるかクソゲーになるかの大きな決め手。 いろいろな方法があるけど、今回はその考え方うちの一つを紹介します。
ちょっと考えてみよう。半径2センチの円と、3センチの円があったとしよう。この2つが当たっているかどうかを判定する条件はなんだろう?

そう、中心同士の距離が2+3=4センチ以下かどうか、だよね。
そのとおりなんだけど、pyxelの画面では、こんな感じのx座標、y座標で、キャラクターや物体の場所が表される。この2つが当たってるかどうか、どうやって判定する?

三平方の定理を思い出してみよう。
こういう直角三角形をおいてみると、円の中心同士の距離がわかる
?の部分を求めるには
?の2乗 = 4の2乗 + 3の2乗
なので、?は5。つまり、当たっとるやないかい!ってこと すごいぜ数学!!数学大好き!!!
当たり判定について、ストリートファイター開発者の人がより詳しいことを解説しているので、読んでみて ストゼミ:当たり判定の基本
みんなが「なんだよこのクソゲー当たり判定ザルかよww金返せ!!」って言ってる裏では、プログラマーのいろんな工夫や努力があるわけです
今回は弾が長方形で、ちゃんとやると、もうちょっと大変になる。なので、今回は単純に弾の四角が敵に重なってるかどうかで判定します。 該当部分のコードはこんな感じ。
# 敵に当たった弾を削除
for i in range(len(character["tama"]) - 1, -1, -1):
tama_x = character["tama"][i][0]
tama_y = character["tama"][i][1]
# 当たり判定:弾と敵の矩形が重なっているかチェック
if (
tama_x < enemy["x"] + 16
and tama_x + 16 > enemy["x"]
and tama_y < enemy["y"] + 16
and tama_y + 16 > enemy["y"]
):
character["tama"].pop(i)
update関数の中身がバケモンになってきたので、全体のコードを示しておきます。
def update():
print(character["tama"])
# 一番古い弾のデータをチェック
if len(character["tama"]) and character["tama"][0][1] <= -16:
character["tama"].pop(0)
# 敵に当たった弾を削除
for i in range(len(character["tama"]) - 1, -1, -1):
tama_x = character["tama"][i][0]
tama_y = character["tama"][i][1]
# 当たり判定:弾と敵の矩形が重なっているかチェック
if (
tama_x < enemy["x"] + 16
and tama_x + 16 > enemy["x"]
and tama_y < enemy["y"] + 16
and tama_y + 16 > enemy["y"]
):
character["tama"].pop(i)
enemy["x"] = enemy["x"] + enemy["direction"]
if enemy["x"] >= 120 - 16 or enemy["x"] <= 16:
enemy["direction"] = enemy["direction"] * -1
if pyxel.btn(pyxel.KEY_RIGHT) == True:
character["x"] = character["x"] + 1
elif pyxel.btn(pyxel.KEY_LEFT) == True:
character["x"] = character["x"] - 1
if pyxel.btnp(pyxel.KEY_SPACE) == True:
character["tama"].append([character["x"], character["y"] - 16])
for i in character["tama"]:
i[1] = i[1] - 1
このままではゲームが終わらないので、敵のHPがゼロになったらゲームクリア、という感じに実装してみよう
まずは敵(enemy)キャラにHPをもたせよう。enemyの辞書型に「HP」を追加。HPを100にしてみよう
enemy = {
"x": 120 / 2 - 16 / 2,
"y": 20,
"direction": 1,
"HP": 100
}
次に、そのHPを表示する処理を追加。表示を担当するdraw関数に、この一行を追加しよう
def draw():
pyxel.cls(0)
pyxel.blt(character["x"], character["y"], 0, 0, 0, 16, 16, 0)
pyxel.blt(enemy["x"], enemy["y"], 0, 16, 0, 16, 16, 0)
# この1行を追加
pyxel.text(10, 10, f"enemy HP: {enemy["HP"]}", 7)
for i in character["tama"]:
pyxel.blt(i[0], i[1], 0, 32, 0, 16, 16, 0)

では、敵に当たったときにダメージが入る=enemy[“HP”]が減る処理を追加していこう。当たった弾を消す処理のところに書けば良さげ。
def update():
print(character["tama"])
# 一番古い弾のデータをチェック
if len(character["tama"]) and character["tama"][0][1] <= -16:
character["tama"].pop(0)
# 敵に当たった弾を削除
for i in range(len(character["tama"]) - 1, -1, -1):
tama_x = character["tama"][i][0]
tama_y = character["tama"][i][1]
# 当たり判定:弾と敵の矩形が重なっているかチェック
if (
tama_x < enemy["x"] + 16
and tama_x + 16 > enemy["x"]
and tama_y < enemy["y"] + 16
and tama_y + 16 > enemy["y"]
):
character["tama"].pop(i)
# この一行を追加
enemy["HP"] = enemy["HP"] - 10
<以下略>

今のままだと敵のHPがマイナスになってしまってゲームが終わらないので、HPがゼロ以下になったら「GAME CLEAR!」の表示を出すようにしよう
さて、何をどう判定してクリア表示にするかだけど
pyxel.init(120, 120)
pyxel.load(“danmaku.pyxres”)
character = {
“x”: 120 / 2 - 16 / 2,
“y”: 90,
“tama”: []
}
enemy = {
“x”: 120 / 2 - 16 / 2,
“y”: 20,
“direction”: 1,
“HP”: 100
}
game = { “status”: True } <以下略>
敵のHPがゼロ以下になったときに、game["status"]をFalseにしよう。
```python
def update():
print(character["tama"])
# 一番古い弾のデータをチェック
if len(character["tama"]) and character["tama"][0][1] <= -16:
character["tama"].pop(0)
# 敵に当たった弾を削除
for i in range(len(character["tama"]) - 1, -1, -1):
tama_x = character["tama"][i][0]
tama_y = character["tama"][i][1]
# 当たり判定:弾と敵の矩形が重なっているかチェック
if (
tama_x < enemy["x"] + 16
and tama_x + 16 > enemy["x"]
and tama_y < enemy["y"] + 16
and tama_y + 16 > enemy["y"]
):
character["tama"].pop(i)
enemy["HP"] = enemy["HP"] - 10
# ここを追加
if enemy["HP"] <= 0:
game["status"] = False
<以下略>
では、draw()関数を書き換え、game[“status”]がTrueのときはキャラクターなどを表示して、そうじゃないときは GAME CLEAR!を表示するようにしよう
def draw():
pyxel.cls(0)
if game["status"] == True:
pyxel.cls(0)
pyxel.blt(character["x"], character["y"], 0, 0, 0, 16, 16, 0)
pyxel.blt(enemy["x"], enemy["y"], 0, 16, 0, 16, 16, 0)
pyxel.text(10, 10, f"enemy HP: {enemy["HP"]}", 7)
for i in character["tama"]:
pyxel.blt(i[0], i[1], 0, 32, 0, 16, 16, 0)
else:
pyxel.text(40, 50, "GAME CLEAR!", 7)
