人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

反射方向の調整

前回までのプログラムを実行してしばらく遊んでみるとわかりますが、ボールがいつも同じ方向に跳ね返るためまったく面白くありません。プレイヤーができるのはボールを落とさないようにすることだけです。今回は、ボールがパドルに当たる位置によって反射方向が微妙に変わるように改良します。プレイヤーはボールの当たる位置をうまく調整することでブロックを効率よく壊せるようになりゲーム性が出てきます。

breakout05.zip
f:id:aidiary:20100807232143p:plain

反射方向の調整

まず反射方向を調整するとはどういうことか実感するためプログラムを実行してみてください。ボールがパドルに当たる位置によって反射方向が変わることがわかるはずです。パドルの左の方に当てるとボールは斜め左に反射します。パドルの右の方に当てるとボールは斜め右に反射します。パドルの中央に当てるとボールはまっすぐ反射します。前回までとはけっこう違うのですがわかりますか?ではプログラムで書き下せるようにもっと定式化します。下の図を見てください。長っぽそい四角形はパドルで、正方形はボールです。ボールは丸ですが当たり判定は四角になっています。

f:id:aidiary:20100807232144p:plain

まず、ボールがパドルの一番左端に当たる場合ですが、このときのボールのX座標をx1とし、ボールは135度方向に跳ね返ることにします。次にボールがパドルの一番右端に当たる場合の場合ですが、このときのボールのX座標をx2とし、ボールは45度方向に跳ね返ることにします。この2つの値は Ballの定数として定義してます。

    angle_left = 135
    angle_right = 45

では、左端と右端の間で当たったときはどうしましょう?ボールがパドルの真ん中に当たったときはちょうど90度方向に跳ね返したいですね。では、それ以外の場所はどうすればいいでしょう?上の図では「?」と書いた角度です。おそらく、左の?は90度から135度の間ですし、右の?は45度から90度の間と推定できますが、具体的な値はわかりません。実は、こういうときに線形補間という方法が使えます。

線形補間

補間は、簡単に言うと、両端の値はわかっているがその間がわからないときに間の値を予測する方法です。内挿とも言います。線形は、数学では直線と同じ意味です。つまり、線形補間というのは両端を直線で結んだと仮定して、間の値を予測する方法です。さっきの反射方向の例だと左端は135度、右端は45度とわかっているがその間の反射角度がわからない。では、直線がひけると仮定して間の反射角度を予測しようってことです。下の図を見てください。

f:id:aidiary:20100807232145p:plain

横軸はボールがパドルに当たる位置を、縦軸はボールの反射角度を表しています。xとyって書いてますが、画面の位置を表すX座標、Y座標ではないので混乱しないでください(別の記号を使えばよかった・・・)。今、わかっているのは赤丸で書いた場所です。x1の位置ではy1=135度、x2の位置ではy2=45度にすると先ほど決めました。ここでの問題は、x1とx2の間の好きな位置(たとえば、x)での反射角度yを求めることです。両端の間は線形(直線)で補間すると決めたので両端の赤丸の間は直線で結んでいます。この問題は実は中学校の数学で習います。いわゆる直線の方程式というやつです。この直線の方程式を求めてしまえば好きなx(ボールが当たる位置)でのy(反射角度)が求められます。直線の方程式は、直線の傾きをmとしたとき、

 y = m * (x - x1) + y1

です。傾きmは、わかっている赤丸の座標から

 m = (y2 - y1) / (x2 - x1)

で求まります。ではコードを見てみます。実際、上の説明をそのまま実装しただけです。

        # パドルとの反射
        if self.rect.colliderect(self.paddle.rect) and self.dy > 0:
            # パドルの左端に当たったとき135度方向、右端で45度方向とし、
            # その間は線形補間で反射方向を計算
            x1 = self.paddle.rect.left - self.rect.width  # ボールが当たる左端
            y1 = self.angle_left  # 左端での反射方向(135度)
            x2 = self.paddle.rect.right  # ボールが当たる右端
            y2 = self.angle_right  # 右端での反射方向(45度)
            m = float(y2-y1) / (x2-x1)  # 直線の傾き
            x = self.rect.left  # ボールが当たった位置
            y = m * (x - x1) + y1

赤丸の座標を(x1,y1)と(x2,y2)として、直線の傾きmを求めます。ここでは、直線の方程式にボールが当たった位置xを代入して、反射角度yを計算しています。

移動速度の分解

これで反射角度はわかったので次は反射角度に応じてボールの移動速度を(vx,vy)に分解します。下の図を見てください。

f:id:aidiary:20100807232146p:plain

反射角度yはangleと書き直しています(プログラムでは単位を度からラジアンに変換)。ボールの速度speedはangle方向の速度です。これをX方向の速度vxとY方向の速度vyに分解します。この分解方法は、高校の数学で習います。いわゆる三角関数、サイン・コサイン・タンジェントってやつです。結果だけ書きますが、angle方向のベクトルの長さがspeedのときvxとvyは、

 vx = speed * cos(angle)
 vy = speed * sin(angle)

で計算できます。ではコードを見てみます。実際、上の説明をそのまま実装しただけです。

            angle = math.radians(y)
            self.dx = self.speed * math.cos(angle)  # float
            self.dy = -self.speed * math.sin(angle) # float

あとは今までどおりBallのupdate()の中で、位置 (x,y) に速度 (vx,vy) を加えることでボールがangleの反射角度で斜めに移動します。