こんにちは!
前回こんな記事を書きました↓
本記事は『Pythonでレトロゲームを作ろう!』シリーズの”Day7”です!
本記事”Day7”が本シリーズの最終回となります。
【シリーズ構成】
- Day1~Day4:基礎編
- Day5~Day7:応用編
当初の予定では“Day5″もしくは”Day5”を待たずに打ち切りになると考えていました(基礎編+αくらいの内容にする予定でした)
しかし、嬉しいことにTwitter経由で本シリーズが「参考になった」・「勉強になる」という言葉を頂けました!
ありがとうございます!励みになりました!
お陰様で応用編を増やして”Day7″まで続けることができました!感謝!!
Contents
【Day 7】Pyxelでレトロゲームを作ろう!
”Day6”の最後(次回予告)で”くるる”ちゃんから以下のことをお願いされました。
はい!『自動追従マウス』を作ってみました↓
本サイトでは、プレイヤーが操作せずに自動で動くキャラを『人工知能』と呼ぶことにします。
ということで、本記事では『ゲームにおける人工知能』の作り方を説明します。
【補足】ゲームにおける人工知能
人工知能と聞いて『機械学習』を連想する人が多いと思いますが…
私が小さい頃は”マリオ”や”ロックマン”などの敵キャラ(ノンプレイヤーキャラクター)のことを『人工知能/AI/CPUキャラ』などと呼んでいました。
技術の発展と共に抽象的だった『人工知能』という言葉の定義が変わった気もしますが、今でも以下のように説明することがあります。
コンピュータゲームにおける人工知能は、コンピュータゲームにおいて、ノンプレイヤーキャラクター (NPC) の振る舞いに知能があるかのような錯覚を生み出す技法である。主な技法は人工知能 (AI) の既存技術を活用したものである。しかしゲームAIと呼ぶ場合、制御理論・ロボット工学・コンピュータグラフィックス・計算機科学全般の技法を含む様々なアルゴリズムを指して使われることが多い。
本シリーズは『レトロゲーム』関連の記事なので、あまり難しいことは考えずノンプレイヤーキャラクターの”ねずみ”は『人工知能』ということにしますね(*・ω・)ノ♪
人工知能と制御
上記の補足で説明した通り、ゲームAIでは制御理論などを使って、知能のあるゲームキャラを実装します。
今回も制御理論を使って猫にこっそり近づく知能のある”ねずみ”を実装しました。
先に種明かしすると、P制御を使っています。
”はやぶさ先生”が元制御屋を名乗るときは、制御の話をしてくれるよね↓
制御は工業製品で使われる難しいもの!と思われがちです。本サイトで「制御の敷居」を下げられると嬉しいなぁ…なんてことを考えています。
P制御を説明する前に
PID制御(P制御の内容含む)については”ググる”と良質な記事が沢山でてきます。
例えば、以下のサイトなど↓
モーターやロボットアームを例に数式も交えて分かりやすく解説しています。
同じ説明をしてもしょうがないので、本記事では『ゲームを題材にP制御を解説』してみます!
【復習】ゲームとベクトル
“Day4”で『ベクトル』について説明したのを覚えていますか?
内容を忘れてしまった人は、読み返してほしくないなぁ
正解!
また、A点からB点に移動するということは…
- x軸方向:A点から Xb – Xa だけ移動する
- y軸方向:A点から Yb – Ya だけ移動する
という意味でしたね。
“Day4”では点Aが猫の座標・点Bがマウスカーソルの座標でした。
今回は以下のように点Aが敵キャラ(ねずみ)の座標・B点が猫の座標と定義します↓
なので、敵キャラ(ねずみ)が猫に近づくには…
- x軸方向:A点から Xb – Xa だけ移動する
- y軸方向:A点から Yb – Ya だけ移動する
で実現できますね。
しかし、“Day4”と同じ方法で点A座標を点B座標に更新すると、敵キャラが”一瞬で”猫に追いついてしまいます。
この問題を解消し、猫にこっそり近づく”ねずみ”を実現するために”P制御”を使います。
制御/物理/数学のプログラミング
ゲームに限らず、抽象的な表現をプログラムで実現するのは非常に難しいです!
今回の場合”こっそり近づく”が抽象的なためプログラミング困難です。
抽象的な表現:こっそり近づく
なので”こっそり近づく”とはどういうことか?を考えてみます!
つまり、身近な物理現象で”こっそり近づく”を表現できないかを考えます。
ばねが良いと思いませんか?
『伸ばす長さに比例して、素早く縮まる特性をもつ”ばね”』
『猫までの距離に比例して、近づく移動量(移動速度)を変える”ねずみ”』
似ていますね。
具体的な表現:猫までの距離に比例して、”ねずみ”の移動量を変化させる
…という感じで、実装したいことを具体的に考えるとプログラミンしやすいです。
抽象的な表現はプログラミング困難ですが、物理現象に変換できればプログラミングしやすくなります。
ばねの特性
もう少し詳しく”ばねの特性”を説明します。
何も力を加えていない静止状態のばねの長さを”自然長”と呼びます。
ばねを伸ばしたり/縮めたりすると”自然長”に戻ろうとする特性があり、このとき働く力を”復元力”と呼びます。
また、ばねの長さに比例して”復元力”は強くなります。この”復元力”は中学生の頃に学ぶ『フックの法則』で算出することができます。
【フックの法則】 F = k・x(復元力:F, 自然長からの長さ:x, ばね定数:k)
技術や知識の積み重ねが大事です!
ばねとP制御
【P制御】の”P”は”Proportional:比例”の頭文字なので【P制御】のことを【比例制御】と呼ぶこともあります。
比例は上記の【ばねの特性】でも出てきたキーワードです。
【ばねの特性】自然長に戻すために、長さに比例した復元力が発生する
一方【P制御】は…
【P制御】目標値に近づくために、目標までの偏差に比例した入力値を制御する
と考え、以下の数式を使います。
【P制御】U = Kp・e(入力値:U, ゲイン:Kp, 偏差:e)
つまり、【P制御】は【ばねの特性】と同じように考えることができます。
【P制御】の入力値:Uや偏差:eは制御対象により変わります。
今回は以下のように定義します。
- 制御対象:画像上の”ねずみ”
- 偏差:猫までの距離(目標までの距離)
- 入力値:移動量(更新座標)
ふわぁーー💤という大きな欠伸が聞こえてきた。
いつもより静かだと思ったら…眠かったのか!笑
大きい”くるる”ちゃんの目が、更に大きく”カッ”と開いた!
”やる気スイッチ”入りました!笑
PyxelとP制御
猫と”ねずみ”をばねで接続します!
ばねが伸びるとどうなるか?を考えてみます↓
【ばねが伸びる】
- 猫が”ねずみ”から逃げる
- ばねが伸びる
- 復元力:大
- ”ねずみ”が走って追いかけてくる(移動量:大)
ばねが縮むとどうなるか?を考えてみます↓
【ばねが縮む(自然長に近づく)】
- ”ねずみ”が猫に追いつく
- ばねが縮む(目標に近づく=偏差が小さくなる)
- 復元力:小
- ”ねずみ”がこっそり近づく(移動量:小)
より正確には、x軸方向・y軸方向それぞれに”ばね”を接続するイメージです↓
こうすることで、x軸方向とy軸方向で移動量を制御できます。
その通り!
じゃあ、ばねを伸ばす以外に”ねずみ”の移動量を増やすにはどうすれば良いかな?
大正解!
【P制御】なら”ゲイン:Kp”を大きくすれば、“猫までの距離:e”に比例して”ねずみの移動量(更新座標):U”が大きくなるように制御できます!
【P制御】U = Kp・e(入力値:U, ゲイン:Kp, 偏差:e)
苦手な分野(今回の場合は物理や制御)でも自分の好きな分野(ゲームなど)に落とし込めば、学ぶのが楽しくなるかも!
P制御のソースコード
以上を踏まえて、Pyxelを使ったP制御のソースコードは以下の通りです↓
1 2 3 4 5 6 7 8 |
# P制御 ex = (self.mcat.pos.x - self.Enemy.pos.x) ey = (self.mcat.pos.y - self.Enemy.pos.y) Kp = 0.02 if ex != 0 or ey != 0: self.Enemy.update(self.Enemy.pos.x + ex * Kp, self.Enemy.pos.y + ey * Kp, self.mcat.vec) |
今回は【P制御】ですが、本記事をきっかけに”制御”に興味をもった人が【PI制御】や【PID制御】などに挑戦してくれたら、最高に嬉しいです(*・ω・)ノ♪
最終的に完成したソースコード
最終的に完成したソースコードはこちらです↓
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 |
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 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: # 1匹だけ敵キャラを実体化 new_enemy = Enemy(self.IMG_ID2) new_enemy.update(random.randrange(WINDOW_W), random.randrange(WINDOW_H), self.mcat.vec) self.Enemies.append(new_enemy) self.flag = 0 enemy_count = len(self.Enemies) for i in range(enemy_count): # P制御 ex = (self.mcat.pos.x - self.Enemies[i].pos.x) ey = (self.mcat.pos.y - self.Enemies[i].pos.y) Kp = self.Enemies[i].speed if ex != 0 or ey != 0: self.Enemies[i].update(self.Enemies[i].pos.x + ex * Kp, self.Enemies[i].pos.y + ey * Kp, self.mcat.vec) # ====== 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) # ====== 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) App() |
おわりに
『Pythonでレトロゲームを作ろう!Day 7-人工知能-』について説明しました。
ゲーム開発(pyxelの使い方)を題材にして、楽しくプログラミングの勉強ができると良いなーとか考えながら、シリーズ記事を書いています。
今回は元制御屋として書いてみたかった、【P制御】を題材にしました。
この記事が本シリーズの最終回です(総集編を書く予定ですが、チュートリアルとしては本当に最終回です。)
本シリーズで紹介したソースコードを自由にカスタムして、自分オリジナルのゲームを作ってみて下さいね!
「Pythonでレトロゲームを作ろう」のシリーズ記事を全て読むと以下のようなゲームを作れるようになります!
記事もソースコードも無料で公開してるので、気楽にサイトに遊びにきて下さいね!#pyxel
【Pythonでレトロゲームを作ろう -Day1~Day7】https://t.co/dFlpkKja3e pic.twitter.com/xutyLORMiq
— はやぶさ (@Cpp_Learning) January 6, 2019
総集編でソースコードを公開しますね!
本シリーズ楽しかったかな?Twitterなどで感想をツイートしてくれると嬉しいです!
Twitterアカウント:”はやぶさ”@Cpp_Learning
”くるる”ちゃんも待っているので、気軽にサイト『はやぶさの技術ノート』に遊びに来てくださいね(*・ω・)ノ♪
ご愛読ありがとうございました!
『Pythonでレトロゲームを作ろう!』シリーズ(完)