こんにちは!
前回こんな記事を書きました↓
本記事は『Pythonでレトロゲームを作ろう!』シリーズの”Day5”です!
Contents
【Day 5】Pyxelでレトロゲームを作ろう!
”Day4”で猫を自由自在にマウスで操作できるようになりました↓
これだけでも十分楽しいし、工夫次第でクリックゲームなどを作れます!
ただ…
という”くるる”ちゃんの熱い要望に応えて作ってみました↓
連続攻撃!!#pyxel #pyxelでゲーム作り pic.twitter.com/PtGlvjvuGW
— はやぶさ (@Cpp_Learning) December 23, 2018
ただ、可愛い猫ちゃんが”ショット”っていうのは、少し穏やかじゃない気がしたので…
以降からは”ボールを投げる”という柔らかい表現を使いたいと思います!
開発手順を説明する前に…
Day4記事の最後で説明した通り、Day1~Day4で得た知識だけで多くのことができます。
なので、時間に余裕のある人は”自力”で猫がボールを投げる動作を実装してみてほしい!
悩みながら手を動かすのが一番勉強になります!
作り手によって、コードの書き方は様々です!
あなたが作ったソースコードとこれから”くるる”ちゃんが作るソースコードを見比べて…
というものがあれば、どんどん吸収して下さいね(*・ω・)ノ♪
いっぱい失敗して!いっぱい悩んで!楽しく学びましょう!そして、本やネットなどから得られる知識をどんどん吸収しましょう!
猫がボールを投げる動作を実装する
さてと…
”くるる”ちゃんが…燃えている!!
じゃあ、ここからは”くるる”ちゃん主体でお願いねー
調査
以下の2つの記事に書いてあるけど、ゲームに限らずソフトを開発するときは、まず調査をするんだよね↓
ふふふっ!実は既に目星はついている!
以下の記事で”くるる”はショット攻撃をしたことがあるのだ!!ドヤァ
しかもソースコード開発は”はやぶさ先生”に丸投げしたから模範解答だと思うのよね(*・ω・)ノ♪
マネしよーっと♪
スポーツでプロの真似をしたことはありませんか?プログラミングもスポーツ同様に真似をすることで上達できますよ!ネット上にはプロの作った上質なソースコードがたくさん転がっています!感謝しつつ、マネさせて頂きましょう!
ボールのクラス化
ボールってオブジェクトだからクラス化できそう!
Day3で作った”猫クラス”をベースに情報を追記して”みるる”!
拡張性を考えて、色や大きさの違うボールも投げれるような設計にしたいなぁ↓
・ボールの大きさに変化をつける
・ボールの飛んでいく方向を猫の向きに合わせる#pyxel pic.twitter.com/O3YdGLBYWG— はやぶさ (@Cpp_Learning) December 12, 2018
まずは、色と大きさの違うボール画像を大量に準備して、それを実体化するクラスを作れば…
そうだった!Day1に書いてあるけど…
画像は0~2しか挿入できないんだった!
・blt(x, y, img, u, v, w, h, [colkey])
イメージバンクimg(0-2) の (u, v) からサイズ (w, h) の領域を (x, y) にコピーする。w、hそれぞれに負の値を設定すると水平、垂直方向に反転する。colkeyに色を指定すると透明色として扱われる引用元:pyxel|GitHub
困った…ん?そういえばDay2記事のポイントで…
って書いてあった!APIリファレンスに何かヒントが…あった!!
・circ(x, y, r, col)
半径r、色colの円を (x, y) に描画する引用元:pyxel|GitHub
円などの幾何学図形を書くためのAPIリファレンスが準備されてる!
じゃあ画像の情報は無しで、猫クラスをベースに色/サイズ、あと拡張性も考えて球速の情報(属性)も追加して…
ボールクラスをこんな感じにしてみたよ↓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 |
update関数については、まだ悩んでるけど…球速以外は全て更新できるようにした!
インスタンス生成 -失敗編-
次はボールクラス(設計図)からインスタンス生成(実体化)するよー
Day3記事で猫クラスから3匹の猫を実体化させたことあるし、簡単だね!
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 |
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.mouse(True) # make instance self.mcat = cat(self.IMG_ID1) self.ball_0 = Ball() self.ball_1 = Ball() self.ball_2 = Ball() ~ self.ball_98 = Ball() self.ball_99 = Ball() # self.mouse_count = 0 pyxel.run(self.update, self.draw) |
ボール100個くらい準備しとけば良いよね(18行目~)
……
”くるる”ちゃんはボール”966″個分のコードを書いた時点で…飽きて座り込んでしまった!
GAME OVER!?
”くるる~”って振り返る”くるる”ちゃんが今日も可愛い(*・ω・)ノ♪
ここからは、私(はやぶさ)主体に戻します。
インスタンス生成 -成功編-
”くるる”ちゃんのように複数のインスタンスを予め用意するコードも正解です!
しかし、インスタンスをいくつ用意すれば十分なのか?が不明確な場合、”くるるちゃん”のように必要以上のインスタンスを準備することになり…疲れます!爆
今回の場合は、ユーザーがマウスクリックした時にインスタンス生成するようにします!
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 |
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.mouse(True) # make instance self.mcat = cat(self.IMG_ID1) self.Balls = [] # self.mouse_count = 0 pyxel.run(self.update, self.draw) def update(self): if pyxel.btnp(pyxel.KEY_Q): pyxel.quit() |
実はリストってインスタンスも格納できます↑(17行目)
なので入れ物だけ用意しておいて…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def update(self): if pyxel.btnp(pyxel.KEY_Q): pyxel.quit() # ====== 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) |
左のマウスクリックが押されたときにインスタンスを生成(7行目)
生成したインスタンスはリストに追加していけばOK!(16行目)
こうすれば、ユーザーがクリックした分だけボールを実体化できます。
左クリックが押されたとき、猫の前方でボールを実体化するようにしました。
最初に各座標を確認してみて下さい↓
【各座標について】
- 猫の座標(mcat.pos.x, mcat.pos.y)
- 猫画像の幅(CAT_W)= 16, 猫画像の高さ(CAT_H)= 16
- 猫画像の中心座標(mcat.pos.x + CAT_W/2, mcat.pos.y + CAT_H/2)
【ボールの初期座標】
- 猫が右向きのときは、猫の中心座標から+6pixelがボールの初期位置
- 猫が左向きのときは、猫の中心座標から-6pixelがボールの初期位置
インスタンスの破棄
”くるる”ちゃん問題です!
それは”くるる”ちゃんのことだよね…?笑
正解は「メモリがパンクする恐れがある!」でしたー
なので、ボールが何かと衝突したら消滅…つまりインスタンスを破棄しましょう!
とりあえず、今回はボールが画面端に衝突したらインスタンスを破棄させます。
ボールが飛ぶ
次に「投げたボールが飛んでいく」について簡単に説明します。
前回までの「猫をマウスで動かす」や今まで説明してきた「左クリックでボールを投げる」は、ユーザが制御しています。
しかし、「投げたボールが飛んでいく」というのはユーザが関与しない動作です。
つまり、猫の手を離れたボールはユーザーが制御することなく”勝手に”前方に飛んでいけば良いのです。
以上の「インスタンスの破棄」と「ボールが飛ぶ」を実装したコードが↓です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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) else: del self.Balls[i] break |
【ボールを投げる処理の簡易フロー】
- ボール数をカウント(1行目)
- ボールが画面内に存在するか確認(3行目)
- ボールが画面内に存在する場合は飛ぶ向きを考慮してx座標を更新する※(5~12行目)
- ボールが画面外に存在する場合はインスタンスを破棄(14行目)
- ②~④を全ボールが消滅するまで繰り返す
※正確には拡張性を考慮してx座標以外も更新しています(ボールclass参照)
最終的に完成したソースコード
最終的に”くるる”ちゃんが完成させたコードはこちらです↓
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 |
import pyxel WINDOW_H = 120 WINDOW_W = 160 CAT_H = 16 CAT_W = 16 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 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.mouse(True) # make instance self.mcat = cat(self.IMG_ID1) self.Balls = [] # self.mouse_count = 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 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) else: del self.Balls[i] 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) App() |
おわりに
『Pythonでレトロゲームを作ろう!Day 5-ショット攻撃-』について説明しました。
ゲーム開発(pyxelの使い方)を題材にして、楽しくプログラミングの勉強ができると良いなーとか考えながら、シリーズ記事を書いています。
今回は”ショット攻撃”を題材に、以下のことを学んでほしいと思って書きました。
- 柔軟に複数インスタンスを生成する方法
- インスタンスの破棄
- 座標(ボールなど)の自動更新
(完…?)
おまけ -本シリーズ続けます-
ぐすん…
すごい楽しかったよー(´;ω;`)
『本シリーズもう少しだけ続けます!』
当初の予定では、人気がなければ5dayを待たずに打ち切る予定でした。続けても5dayで完結する簡単なクリックゲームにする予定でしたが…
意外と好評なので、もう少し続けることにしました!
本記事楽しかったかな?次回もお楽しみに~