bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

Netwireでゲームを作成する(13)

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\)を満たす。これを解けばよいのだが複雑である。この記事の目的は、ふるまいの切り替えやイベントを理解することにあるので、ここでは近似的な値を用いた。興味ある人は、正確な値を求めてほしい。

その他に、この記事では、ピンポン玉の跳ね返りの位置も厳密ではない。簡単にするために、ピンポン玉の中心で跳ね返るようにしているが、ピンポン玉を大きくすると、違和感が増してくると思う。興味のある人はピンポン玉の表面では跳ね返るようにして欲しい。