16.グラフィックスを含めて完成させる
前回までの記事で、ピンポン玉をラケットで打つ処理の部分までを完成させたが、グラフィックスの部分が完成していなかったため、ゲームそのものを楽しむことはできなかった。今回は、グラフィックスの部分を実装し、ゲーム全体を完成させた。
出来上がったゲームで遊んだ動画は次の通りである。
今回は、グラフィックスにOpenGLとGLFWを用いた。描画したのは、ピンポン玉、ラケット、壁である。これらは描画するためには、すべて、凸多角形で表わす。そして、凸多角形を構成する頂点の座標を反時計回りのリストで表わし、これをレンダリングする。プログラムは次のようになる。
{-# LANGUAGE Arrows #-} -- module Main where import Prelude hiding ((.),) import Control.Wire import Control.Monad.IO.Class() import FRP.Netwire() import Graphics.Rendering.OpenGL import Graphics.UI.GLFW import Data.IORef import Linear.V2 import Ball import Racket import Configure type Point = (Double, Double) type Polygon = [Point] renderPoint :: Point -> IO () renderPoint (x, y) = vertex $ Vertex2 (realToFrac x :: GLfloat) (realToFrac y :: GLfloat) generatePointsForBall :: Ball -> Polygon generatePointsForBall (Ball (V2 x y) r) = map (\t -> (x+r*cos (t), y+r*sin (t))) [0,0.2..(2*pi)] generatePointsForRacket :: Racket -> Polygon generatePointsForRacket (Racket (V2 x y) (V2 w h)) = [ (x, y) , (x + w, y) , (x + w, y + h) , (x, y + h) ] generatePointsForLeftWall :: Polygon generatePointsForLeftWall = [ (-1, -1) , (- wall, -1) , (- wall, 1) , (-1, 1) ] generatePointsForRightWall :: Polygon generatePointsForRightWall = [ (1, 1) , (wall, 1) , (wall, -1) , (1, -1) ] runNetwork :: (HasTime t s) => IORef Bool -> Session IO s -> Wire s e IO a (Ball, Racket) -> IO () runNetwork closedRef session wire = do pollEvents let color3f r g b = color $ Color3 r g (b :: GLfloat) closed <- readIORef closedRef if closed then return () else do (st , session') <- stepSession session (wt', wire' ) <- stepWire wire st $ Right undefined case wt' of Left _ -> return () Right (b,r) -> do clear [ColorBuffer] --color (Color3 1 0 0) color3f 1.0 0.8 0.6 renderPrimitive Polygon $ mapM_ renderPoint $ generatePointsForBall b color3f 0.8 0.2 0.2 renderPrimitive Polygon $ mapM_ renderPoint $ generatePointsForRacket r color3f 0.7 0.7 0.7 renderPrimitive Polygon $ mapM_ renderPoint $ generatePointsForLeftWall renderPrimitive Polygon $ mapM_ renderPoint $ generatePointsForRightWall swapBuffers runNetwork closedRef session' wire' game :: HasTime t s => Wire s () IO a (Ball, Racket) game = proc _ -> do r <- racket corner boundary -< () b <- ball radius gInit vInit pInit -< r returnA -< (b, r) main :: IO () main = do initialize openWindow (Size 640 640) [DisplayRGBBits 8 8 8, DisplayAlphaBits 8, DisplayDepthBits 24] Window closedRef <- newIORef False windowCloseCallback $= do writeIORef closedRef True return True runNetwork closedRef clockSession_ game closeWindow
上記のプログラムで、generatePointsForXXが凸多角形の頂点を反時計回りにリストにする関数である。また、renderPrimitiveが多角形を描画する関数である。
関数runNetworkがゲームの進行に合わせて描画する関数である。
なお、このゲームのプログラムコードはGitHubにアップロードしたので、参考にして欲しい。github.com
17.改善点
ピンポン玉を跳ね返し続けると、段々にピンポン玉(の最高点の位置)が上のほうに上がって行くことに気が付く。
これは、ピンポン玉がラケットに当たって跳ね返るときの処理を少しさぼっていることによる。すなわち、反発した時の速度が実際よりは少し大きな値が与えられていることによる。これは次のように説明できる。
プログラムの処理の中で、ピンポン玉がラケットに当たる直前のセッション時間(計算を行う離散的な時間)を\(t\)とし、そん時の\(y\)軸方向の速度と位置を\(-v_t, p_t\)とする。なお、\(-v_t<0\)である。すなわち、落下している。
また、ピンポン玉が当たった直後のセッション時間を\(t+dt\)とする。
もし、ラケットに当たらずに、ピンポン玉が落下したとすると、\(t+dt\)での\(y\)軸方向の速度は、\(v_{t+dt}=-v_t-g \times dt<0\)となる。また、位置は\(p_{t+dt}=p_t-v_t \times dt - \frac{1}{2} \times g \times dt^2 \)となる。
ピンオン玉がラケットに当たった時間を\(t+dt'\)とすると、\(0 < dt' < dt\)となる。これより、\( v_{t+dt'} = - v_t - g \times dt' \)である。なお、\( v_{t+dt} < v_{t+dt'} <0 \)である。すなわち、\( |v_{t+dt}| >|v_{t+dt'}| \)である。
ラケットに衝突した時の速度は\(|v_{t+dt'}|\)であるが、プログラムでは、\(|v_{t+dt}|\)を与えた。このため、実際よりも大きな速度を与えているため、ピンポン玉の最高点の位置が跳ね返すたびに段々に上がっていくになる。
この事実を知って、正確な値を返せばよいのではということになるが、\(dt'\)を求めることはそれほど簡単ではない。いま、ピンポン玉が当たるラケットの面の\(y\)座標での位置を\(p_0\)とする。この時、\(p_t>p_0>p_{t+dt}\)である。\(dt'\)は\(p_t-v_t \times dt' - \frac{1}{2} \times g \times dt' ^2 = p_0\)を満たす。これを解けばよいのだが複雑である。この記事の目的は、ふるまいの切り替えやイベントを理解することにあるので、ここでは近似的な値を用いた。興味ある人は、正確な値を求めてほしい。
その他に、この記事では、ピンポン玉の跳ね返りの位置も厳密ではない。簡単にするために、ピンポン玉の中心で跳ね返るようにしているが、ピンポン玉を大きくすると、違和感が増してくると思う。興味のある人はピンポン玉の表面では跳ね返るようにして欲しい。