こんにちは!
前回こんな記事を書きました↓
本記事は『Pythonでレトロゲームを作ろう!』シリーズの”Day6”です!
【シリーズ構成】
- Day1~Day4:基礎編
- Day5~:応用編
Contents
【Day 6】Pyxelでレトロゲームを作ろう!
Day5の記事でショット攻撃ができるようになりました!
なので…敵キャラに向かって撃ってみましょう!
ノリノリの”くるる”ちゃんが今日も可愛い(*・ω・)ノ♪
敵キャラを倒す
「本記事を読むと、どんなソフトが作れるようになるか?」が気になると思うので、先にデモを見せちゃいますね!
それでは…
……そんなんじゃダメでしょ!!
今日の”くるる”ちゃんは…なんかスゴイぞ!笑
要するに『ショット攻撃でねずみ(敵キャラ)を倒すゲーム』の作り方を本記事で説明していきます。
”くるる”ちゃんが色々と脚色してくれたけど…可愛い猫ちゃんが”ショット攻撃”というのは、少し穏やかじゃないので、以降からは”ボールを投げる”という柔らかい表現を使います!
Game Over!
ふふふ…
ねずみに当たると【Game Over!】だよ
楽しく勉強しよー♪(by くるる)
当たり判定とは
ん?つまり今回は…
いやいや。どちらも『当たり判定』という処理で実現できるよー
一言で説明すると『オブジェクト同士が当たったか(衝突したか)どうかを判定する処理』のことです。
今回の場合は…
【当たり判定の例】
- ボールが”ねずみ”に当たったかを判定
- ”ねずみ”が猫に当たったかを判定
となります。
また、ゲームを面白くするために、当たり判定後のルールを以下のようにします。
【ルール】
- ボールが”ねずみ”に当たった:”ねずみ”を退治
- 猫が”ねずみ”に当たった:Game Over!
「”ねずみ”を退治」というのは「”ねずみ”のインスタンスを破棄する」という意味です。
という人はDay5の記事を読んでほしいなぁ
- 当たり判定:オブジェクト同士が当たったどうかを判定する処理
- 手を動かして楽しくプログラミングを勉強しましょう!
Pyxelと当たり判定
当たり判定…
基礎が大事だよ!
【勉強の順番】
- 「基礎 ⇒ 応用」
- 「応用 ⇒ 基礎」
勉強の順番は①・②どちらでも問題ありません。
(本シリーズは①を採用しています)
ただし、やりたいことが増えてくると『”基礎”が身に付いていない⇒応用できない!』というシーンが必ず出てきます。
無言の敬礼がカッコイイぞ”くるる”ちゃん!
勉強の”順番”や”方法”は好みもあるので、自分に合うスタイルでOKです!ただし、基礎は疎かにしてほしくないなぁ
当たり判定 -ボール💣敵キャラ編-
”くるる”ちゃん【画像の基礎】とは?
【画像の基礎】
- 画像は画素(pixel)の集合体
- 画素(pixel)の位置は座標で表現する
- 座標の原点は、基本的に画像の左上
- 画素はRGB成分を持っている(Pyxelの場合は16色)
- 複数の画像を重ねる場合でも各画像が↑の性質をもつ
↑のポイントの内、特に画像における座標の知識が身についていると『当たり判定』を”スッと”理解できます!
敵キャラの座標は以下の通りです↓
『赤枠内の領域(座標)は全て敵キャラ』だと考えれば…
『赤枠にボールが触れた瞬間に当たり!』と判定すれば良いです↓
これを数式の落とし込むと…
【ボール💣敵キャラの当たり判定】
enemy.pos.x < ボールのx座標 < enemy.pos.x + ENEMY_W
かつ
enemy.pos.y < ボールのy座標 < enemy.pos.y + ENEMY_H
のとき”当たり”と判定する!
1 2 3 4 5 6 7 |
if ((self.Enemie.pos.x < self.Ball.pos.x) and (self.Ball.pos.x < self.Enemie.pos.x + ENEMY_W) and (self.Enemie.pos.y < self.Ball.pos.y) and (self.Ball.pos.y < self.Enemie.pos.y + ENEMY_H)): # 消滅(敵インスタンス破棄) del self.Enemie break |
正解!
ただし、それだとボール1つ 敵キャラ1匹にしか対応していないので、複数に対応できるようにします↓
1 2 3 4 5 6 7 8 9 10 11 |
ball_count = len(self.Balls) for i in range(ball_count): enemy_count = len(self.Enemies) for j in range(enemy_count): if ((self.Enemies[j].pos.x < self.Balls[i].pos.x) and (self.Balls[i].pos.x < self.Enemies[j].pos.x + ENEMY_W) and (self.Enemies[j].pos.y < self.Balls[i].pos.y) and (self.Balls[i].pos.y < self.Enemies[j].pos.y + ENEMY_H)): # 消滅(敵インスタンス破棄) del self.Enemies[j] break |
【ボール💣敵キャラの当たり判定フロー】
- ボール数をカウント(1行目)
- 敵キャラ数をカウント(3行目)
- i番目のボールとj番目の敵キャラの当たり判定(5~8行目)
- ”当たり”なら敵キャラのインスタンス破棄(10行目)
- ②~⑤を繰り返す※
※1個目のボールと全ての敵キャラとの『当たり判定』
⇒2個目のボールと全ての敵キャラとの『当たり判定』
⇒…
をボールが最後の1個になるまで繰り返します
大正解!
一読で理解できた人は”くるる”ちゃんと一緒に”ドヤ顔”して下さい!笑
理解できなかった人…大丈夫です!【本シリーズの基礎編】をもう一度読み返してみて下さい!
今度は「応用 ⇒ 基礎」の順に学んでみましょう(*・ω・)ノ♪
- オブジェクトの領域(座標)を意識する
- 複数オブジェクトの当たり判定は for文 で書ける
当たり判定 -猫💣敵キャラ編-
続いて猫と敵キャラの『当たり判定』について説明します。
1 2 3 4 5 6 7 8 9 |
enemy_count = len(self.Enemies) for i in range(enemy_count): # 当たり判定(敵キャラと猫) if ((self.mcat.pos.x < self.Enemies[i].pos.x) and (self.Enemies[i].pos.x < self.mcat.pos.x + CAT_W) and (self.mcat.pos.y < self.Enemies[i].pos.y) and (self.Enemies[i].pos.y < self.mcat.pos.y + CAT_H)): # Game Overフラグを立てる self.GameOver_flag = 1 |
…おしい!本当におしい!!
確かに上記コードで以下のシーンの敵キャラが”当たっている”と判定できます。
しかし、以下の①~③の敵キャラは”当たっていない”と判定されます。
その理由は、判定条件が以下の通りだったからです。
『敵キャラの左上座標が猫の領域(座標)に入ったか?』で”当たり判定”
左上座標のみではなく…
『敵キャラの領域(座標)が猫の領域(座標)に入ったか?』で”当たり判定”
が正解です。
なお、実装するときは『”領域内で真っ先に接触する座標”が当たったかどうか?』で判定すれば良いです。
今回の場合、猫より敵キャラの方が小さいので、”敵キャラ領域の四角形の頂点”で当たり判定ができます。
ゲーム次第では「前方からの攻撃をガード」や「ボスキャラの弱点部分(の座標)のみ攻撃が有効」などのルールがあります。しかし、今回は猫と”ねずみ”が当たれば【Game Over!】(接触箇所の制約なし)というルールでしたね。
以上を考慮した『当たり判定』のコードが以下です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
enemy_count = len(self.Enemies) for i in range(enemy_count): # 当たり判定(敵キャラと猫) if ((self.mcat.pos.x < self.Enemies[i].pos.x + ENEMY_W) and (self.Enemies[i].pos.x + ENEMY_W < self.mcat.pos.x + CAT_W) and (self.mcat.pos.y < self.Enemies[i].pos.y + ENEMY_H) and (self.Enemies[i].pos.y + ENEMY_H < self.mcat.pos.y + CAT_H) or (self.mcat.pos.x < self.Enemies[i].pos.x) and (self.Enemies[i].pos.x < self.mcat.pos.x + CAT_W) and (self.mcat.pos.y < self.Enemies[i].pos.y + ENEMY_H) and (self.Enemies[i].pos.y + ENEMY_H < self.mcat.pos.y + CAT_H) or (self.mcat.pos.x < self.Enemies[i].pos.x + ENEMY_W) and (self.Enemies[i].pos.x + ENEMY_W < self.mcat.pos.x + CAT_W) and (self.mcat.pos.y < self.Enemies[i].pos.y) and (self.Enemies[i].pos.y < self.mcat.pos.y + CAT_H) or (self.mcat.pos.x < self.Enemies[i].pos.x) and (self.Enemies[i].pos.x < self.mcat.pos.x + CAT_W) and (self.mcat.pos.y < self.Enemies[i].pos.y) and (self.Enemies[i].pos.y < self.mcat.pos.y + CAT_H)): # Game Overフラグを立てる self.GameOver_flag = 1 |
また失敗しちゃった。。
むしろ挑戦した人しか失敗できないからね!失敗から何を学ぶかが重要だよ!!
涙を拭って、無言で敬礼する”くるる”ちゃん…カッコいいぞ!!
- 繰り返す!オブジェクトの領域(座標)を意識する
- 真っ先に触れる座標で当たり判定
- 失敗は”恥”じゃない!失敗から何を学ぶかが重要です!!
最終的に完成したソースコード
”くるる”ちゃんは途中で投げ出さずに『当たり判定』のソフトを完成させました↓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
import pyxel import random WINDOW_H = 120 WINDOW_W = 160 CAT_H = 16 CAT_W = 16 ENEMY_H = 12 ENEMY_W = 12 class Vec2: def __init__(self, x, y): self.x = x self.y = y class cat: def __init__(self, img_id): self.pos = Vec2(0, 0) self.vec = 0 self.img_cat = img_id def update(self, x, y, dx): self.pos.x = x self.pos.y = y self.vec = dx class Ball: def __init__(self): self.pos = Vec2(0, 0) self.vec = 0 self.size = 2 self.speed = 3 self.color = 10 # 0~15 def update(self, x, y, dx, size, color): self.pos.x = x self.pos.y = y self.vec = dx self.size = size self.color = color class Enemy: def __init__(self, img_id): self.pos = Vec2(0, 0) self.vec = 0 self.speed = 0.02 self.img_enemy = img_id def update(self, x, y, dx): self.pos.x = x self.pos.y = y self.vec = dx class App: def __init__(self): self.IMG_ID0_X = 60 self.IMG_ID0_Y = 65 self.IMG_ID0 = 0 self.IMG_ID1 = 1 self.IMG_ID2 = 2 pyxel.init(WINDOW_W, WINDOW_H, caption="Hello Pyxel") pyxel.image(self.IMG_ID0).load(0, 0, "assets/pyxel_logo_38x16.png") pyxel.image(self.IMG_ID1).load(0, 0, "assets/cat_16x16.png") pyxel.image(self.IMG_ID2).load(0, 0, "assets/animal_mouse.png") # pyxel.mouse(True) # make instance self.mcat = cat(self.IMG_ID1) self.Balls = [] self.Enemies = [] # flag self.flag = 1 self.GameOver_flag = 0 pyxel.run(self.update, self.draw) def update(self): if pyxel.btnp(pyxel.KEY_Q): pyxel.quit() # ====== ctrl cat ====== dx = pyxel.mouse_x - self.mcat.pos.x # x軸方向の移動量(マウス座標 - cat座標) dy = pyxel.mouse_y - self.mcat.pos.y # y軸方向の移動量(マウス座標 - cat座標) if dx != 0: self.mcat.update(pyxel.mouse_x, pyxel.mouse_y, dx) # 座標と向きを更新 elif dy != 0: self.mcat.update(pyxel.mouse_x, pyxel.mouse_y, self.mcat.vec) # 座標のみ更新(真上or真下に移動) # ====== ctrl enemy ====== if self.flag == 1: # 4匹の敵キャラを実体化 new_enemy = Enemy(self.IMG_ID2) new_enemy.update(WINDOW_W/2, WINDOW_H/2 + 30, self.mcat.vec) self.Enemies.append(new_enemy) new_enemy = Enemy(self.IMG_ID2) new_enemy.update(WINDOW_W/2 + 30, WINDOW_H/2 + 30, self.mcat.vec) self.Enemies.append(new_enemy) new_enemy = Enemy(self.IMG_ID2) new_enemy.update(WINDOW_W/2 - 30, WINDOW_H/2 + 30, self.mcat.vec) self.Enemies.append(new_enemy) new_enemy = Enemy(self.IMG_ID2) new_enemy.update(WINDOW_W/2 - 60, WINDOW_H/2 + 30, self.mcat.vec) self.Enemies.append(new_enemy) self.flag = 0 enemy_count = len(self.Enemies) for i in range(enemy_count): # 当たり判定(敵キャラと猫) if ((self.mcat.pos.x < self.Enemies[i].pos.x + ENEMY_W) and (self.Enemies[i].pos.x + ENEMY_W < self.mcat.pos.x + CAT_W) and (self.mcat.pos.y < self.Enemies[i].pos.y + ENEMY_H) and (self.Enemies[i].pos.y + ENEMY_H < self.mcat.pos.y + CAT_H) or (self.mcat.pos.x < self.Enemies[i].pos.x) and (self.Enemies[i].pos.x < self.mcat.pos.x + CAT_W) and (self.mcat.pos.y < self.Enemies[i].pos.y + ENEMY_H) and (self.Enemies[i].pos.y + ENEMY_H < self.mcat.pos.y + CAT_H) or (self.mcat.pos.x < self.Enemies[i].pos.x + ENEMY_W) and (self.Enemies[i].pos.x + ENEMY_W < self.mcat.pos.x + CAT_W) and (self.mcat.pos.y < self.Enemies[i].pos.y) and (self.Enemies[i].pos.y < self.mcat.pos.y + CAT_H) or (self.mcat.pos.x < self.Enemies[i].pos.x) and (self.Enemies[i].pos.x < self.mcat.pos.x + CAT_W) and (self.mcat.pos.y < self.Enemies[i].pos.y) and (self.Enemies[i].pos.y < self.mcat.pos.y + CAT_H)): # Game Overフラグを立てる self.GameOver_flag = 1 # ====== ctrl Ball ====== if pyxel.btnp(pyxel.MOUSE_LEFT_BUTTON): new_ball = Ball() if self.mcat.vec > 0: new_ball.update(self.mcat.pos.x + CAT_W/2 + 6, self.mcat.pos.y + CAT_H/2, self.mcat.vec, new_ball.size, new_ball.color) else: new_ball.update(self.mcat.pos.x + CAT_W/2 - 6, self.mcat.pos.y + CAT_H/2, self.mcat.vec, new_ball.size, new_ball.color) self.Balls.append(new_ball) ball_count = len(self.Balls) for i in range(ball_count): if 0 < self.Balls[i].pos.x and self.Balls[i].pos.x < WINDOW_W: # Ball update if self.Balls[i].vec > 0: self.Balls[i].update(self.Balls[i].pos.x + self.Balls[i].speed, self.Balls[i].pos.y, self.Balls[i].vec, self.Balls[i].size, self.Balls[i].color) else: self.Balls[i].update(self.Balls[i].pos.x - self.Balls[i].speed, self.Balls[i].pos.y, self.Balls[i].vec, self.Balls[i].size, self.Balls[i].color) # 当たり判定(敵キャラとボール) enemy_count = len(self.Enemies) for j in range(enemy_count): if ((self.Enemies[j].pos.x < self.Balls[i].pos.x) and (self.Balls[i].pos.x < self.Enemies[j].pos.x + ENEMY_W) and (self.Enemies[j].pos.y < self.Balls[i].pos.y) and (self.Balls[i].pos.y < self.Enemies[j].pos.y + ENEMY_H)): # 消滅(敵インスタンス破棄) del self.Enemies[j] break else: del self.Balls[i] ball_count -= 1 break def draw(self): pyxel.cls(0) pyxel.text(55, 40, "Are you Kururu?", pyxel.frame_count % 16) pyxel.blt(self.IMG_ID0_X, self.IMG_ID0_Y, self.IMG_ID0, 0, 0, 38, 16) # ======= draw cat ======== if self.mcat.vec > 0: pyxel.blt(self.mcat.pos.x, self.mcat.pos.y, self.IMG_ID1, 0, 0, -CAT_W, CAT_H, 5) else: pyxel.blt(self.mcat.pos.x, self.mcat.pos.y, self.IMG_ID1, 0, 0, CAT_W, CAT_H, 5) # ====== draw Balls ====== for ball in self.Balls: pyxel.circ(ball.pos.x, ball.pos.y, ball.size, ball.color) # ====== draw enemy ====== for enemy in self.Enemies: if enemy.vec > 0: pyxel.blt(enemy.pos.x, enemy.pos.y, enemy.img_enemy, 0, 0, -ENEMY_W, ENEMY_H, 11) else: pyxel.blt(enemy.pos.x, enemy.pos.y, enemy.img_enemy, 0, 0, ENEMY_W, ENEMY_H, 11) # ====== draw game over ====== if self.GameOver_flag == 1: pyxel.text(self.mcat.pos.x - 10, self.mcat.pos.y - 5, "GAME OVER", 8) App() |
ねずみ画像(ドット絵)は下記サイトの”ミニマム動物アイコン”を使用しました。
おわりに
『Pythonでレトロゲームを作ろう!Day 6-当たり判定-』について説明しました。
ゲーム開発(pyxelの使い方)を題材にして、楽しくプログラミングの勉強ができると良いなーとか考えながら、シリーズ記事を書いています。
今回はアクションゲームやシューティングゲームなどに使われる『当たり判定』について説明しました。
『当たり判定』の正解だけではなく…
- どんな条件?
- どんな数式?
- なぜ上手くいかない?
- どう改善すれば良い?
という『アルゴリズム』や『考え方(マインド)』まで伝わると嬉しいなぁ
どういたしまして!
次回予告
『Day5 -ショット攻撃-』と本記事『Day6 -当たり判定-』のコードを組み合わせたり、カスタムすれば”自分オリジナルのゲーム”を開発できます。
面白いレトロゲームを作って下さいね!
そんなキラキラした瞳で見つめられたら断れない!笑
なるほど!でもそこはオリジナリティ…
自動追従マウス!!
人工知能とは、ちょっと違う気もするけど、プレイヤーが操作せずに自動でキャラが動いたら人工知能かな?
ちょっと正しい定義は分からないけど、本サイトでは”人工知能”と呼ぶことにします。
次回『Day7 -人工知能-』の記事を最後に本シリーズの最終回とさせて頂きます。
(最後に総集編(まとめ記事)も書く予定ですが、チュートリアルとしては次回が最終回です)
本記事楽しかったかな?次回もお楽しみに~