ゲームをつくろう その5 ブロックくずし

ブロックを複数おけるようになった。
ボールもうごかせるようになった。
というわけで、当たり判定をたしてブロックくずしにしてみる。
当たり判定は最終的に線と線の当たりにおとしこむように実装した。
しかしかなり適当なので、速度が上がってくると枠の外にぬけてしまう。
が、とりあえず今回はゲームっぽいものをつくるということでよい事にしよう。

import Graphics.UI.GLUT
import Graphics.Rendering.OpenGL.GLU
import System
import Data.IORef
import Data.List hiding (intersect)

timerInterval = 16

type Point = (GLdouble, GLdouble)
type Vec2 = (GLdouble, GLdouble)
type Line2 = (Vec2, Vec2)
type ColLine = (Line2, Vec2, Vec2) -- 法線をもった線分 平面を上からみたイメージ

data GameObject = Ball{pos::Point, velocity::Vec2} | -- ボール
                  Block {center::Point, width::GLdouble, height::GLdouble} | -- 破壊可能なブロック
                  ABlock {center::Point, width::GLdouble, height::GLdouble} | -- 破壊不可能なブロック
                  Paddle {center::Point, width::GLdouble, height::GLdouble, velocity::Vec2} | -- 自機
                  Box {center::Point, width::GLdouble, height::GLdouble} -- そとの枠

intersect :: Line2 -> Line2 -> Bool
intersect l1 l2 = let ((x1, y1), (x2, y2)) = l1
                      ((x3, y3), (x4, y4)) = l2 in
                  if (((x1 - x2) * (y3 - y1) + (y1 - y2) * (x1 - x3)) * ((x1 - x2) * (y4 - y1) + (y1 - y2) * (x1 - x4))) <= 0 &&
                     (((x3 - x4) * (y1 - y3) + (y3 - y4) * (x3 - x1)) * ((x3 - x4) * (y2 - y3) + (y3 - y4) * (x3 - x2))) <= 0
                  then True else False

-- 内積
dot :: Vec2 -> Vec2 -> GLdouble
dot v1 v2 = let (x1, y1) = v1
                (x2, y2) = v2 in
            (x1 * x2) + (y1 * y2)

-- ベクトル同士の加算
add :: Vec2 -> Vec2 -> Vec2
add (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

-- ベクトル同士の減算
sub :: Vec2 -> Vec2 -> Vec2
sub v1 v2 = v1 `add` ((-1.0) `mul` v2)

-- ベクトルのスカラー積
mul :: GLdouble -> Vec2 -> Vec2
mul s (x, y) = (s * x, s * y)

intersectBallColLine :: GameObject -> ColLine -> Bool
intersectBallColLine Ball{pos=bp, velocity=bv} cl = let (cll, cln, _) = cl
                                                        bl = (bp, (bp `add` bv)) in
                                                    (bv `dot` cln) < 0 && intersect bl cll

-- 当たり判定用のラインを作る
makeColLines :: GameObject -> [ColLine]
makeColLines Block{center=cn, width=w, height=h} = let (cnx, cny) = cn 
                                                       hw = w / 2.0
                                                       hh = h / 2.0 in
                                                   [(((cnx - hw, cny + hh), (cnx + hw, cny + hh)), (0.0, 1.0), (0.0, 0.0)),
                                                    (((cnx - hw, cny - hh), (cnx + hw, cny - hh)), (0.0, (-1.0)), (0.0, 0.0)),
                                                    (((cnx + hw, cny - hh), (cnx + hw, cny + hh)), (1.0, 0.0), (0.0, 0.0)),
                                                    (((cnx - hw, cny - hh), (cnx - hw, cny + hh)), ((-1.0), 0.0), (0.0, 0.0))]
makeColLines ABlock{center=cn, width=w, height=h} = let (cnx, cny) = cn 
                                                        hw = w / 2.0
                                                        hh = h / 2.0 in
                                                    [(((cnx - hw, cny + hh), (cnx + hw, cny + hh)), (0.0, 1.0), (0.0, 0.0)),
                                                     (((cnx - hw, cny - hh), (cnx + hw, cny - hh)), (0.0, (-1.0)), (0.0, 0.0)),
                                                     (((cnx + hw, cny - hh), (cnx + hw, cny + hh)), (1.0, 0.0), (0.0, 0.0)),
                                                     (((cnx - hw, cny - hh), (cnx - hw, cny + hh)), ((-1.0), 0.0), (0.0, 0.0))]
makeColLines Paddle{center=cn, width=w, height=h, velocity=v} = let (cnx, cny) = cn
                                                                    hw = w / 2.0
                                                                    hh = h / 2.0 in
                                                    [(((cnx - hw, cny + hh), (cnx + hw, cny + hh)), (0.0, 1.0), v),
                                                     (((cnx - hw, cny - hh), (cnx + hw, cny - hh)), (0.0, (-1.0)), v),
                                                     (((cnx + hw, cny - hh), (cnx + hw, cny + hh)), (1.0, 0.0), v),
                                                     (((cnx - hw, cny - hh), (cnx - hw, cny + hh)), ((-1.0), 0.0), v)]
makeColLines Box{center=cn, width=w, height=h} = let (cnx, cny) = cn
                                                     hw = w / 2.0
                                                     hh = h / 2.0 in
                                                    [(((cnx - hw, cny + hh), (cnx + hw, cny + hh)), (0.0, (-1.0)), (0.0, 0.0)),
                                                     (((cnx - hw, cny - hh), (cnx + hw, cny - hh)), (0.0, 1.0), (0.0, 0.0)),
                                                     (((cnx + hw, cny - hh), (cnx + hw, cny + hh)), ((-1.0), 0.0), (0.0, 0.0)),
                                                     (((cnx - hw, cny - hh), (cnx - hw, cny + hh)), (1.0, 0.0), (0.0, 0.0))]


-- ボールとその他オブジェクトとの当たり判定
detectColLine :: GameObject -> ColLine -> GameObject
detectColLine bl@Ball{pos=bp, velocity=bv} cl = if intersectBallColLine bl cl 
                                                 then let (cll, cln, clv) = cl
                                                          vn = mul (bv `dot` cln) cln
                                                          vh = bv `sub` vn 
                                                          nvn = mul (-1.0) vn in
                                                      Ball bp ((nvn `add` vh) `add` (0.2 `mul` clv))
                                                 else bl

attack :: GameObject -> [GameObject] -> [GameObject]
attack ball@Ball{pos=p, velocity=vel} gs = 
    filter (\c -> case c of
                    Block{} -> not $ foldr (||) False (map (intersectBallColLine ball) (makeColLines c))
                    _ -> True
           ) gs

detect :: GameObject -> GameObject -> GameObject
detect (ball@Ball{pos=p, velocity=vel}) Ball{} = ball
detect (ball@Ball{pos=p, velocity=vel}) go = 
    foldl detectColLine ball (makeColLines go)

detectBallWithBlock :: GameObject -> [GameObject] -> GameObject
detectBallWithBlock Ball{pos=p, velocity=vel} [] = Ball p vel
detectBallWithBlock b gs = foldl detect b gs

updateBall :: GameObject -> GameObject
updateBall (Ball{pos=p, velocity=vel}) = 
    let (x, y) = p
        (vx, vy) = vel
    in Ball (x + vx, y + vy) (vx, vy)

updatePaddle :: [Key] -> GameObject -> GameObject
updatePaddle ks Paddle{center=pcn, width=pw, height=ph} = let (pcnx, pcny) = pcn 
                                                              vx = (if ((SpecialKey KeyLeft) `elem` ks) then -1 else 0) +
                                                                   (if ((SpecialKey KeyRight) `elem` ks) then 1 else 0)
                                                             in Paddle ((pcnx + vx), pcny)  pw ph (vx, 0.0)
                                                               
-- ゲームオブジェクトの座標更新
update :: [Key] -> [GameObject] -> [GameObject]
update ks gs = 
    let ball = head $ filter (\o -> case o of 
                                      Ball{} -> True
                                      _ -> False) gs
        blocks = filter (\o -> case o of
                                 Ball{} -> False
                                 Paddle{} -> False
                                 _ -> True) gs
        paddle = head $ filter (\o -> case o of
                                        Paddle{} -> True
                                        _ -> False ) gs
        crntBall = ball
    in [updateBall $ detectBallWithBlock ball $ blocks ++ [paddle]] ++ (attack crntBall blocks) ++ [updatePaddle ks paddle]

-- ゲームオブジェクトの描画
renderGameObject :: GameObject -> IO()
renderGameObject (Ball{pos=p}) = do -- ボールの描画
  let (x, y) = p
  preservingMatrix $ do
    translate (Vector3 x y 0 :: Vector3 GLdouble)
    renderObject Wireframe (Sphere' 0.5 10 10)
renderGameObject (Block{center=cn, width=w, height=h}) = do -- 破壊可能ブロックの描画
  let (x, y) = cn
  renderPrimitive LineStrip $ mapM_ vertex [
                Vertex3 (x + w/2.0) (y + h/2.0) 0.0,
                Vertex3 (x + w/2.0) (y - h/2.0) 0.0,
                Vertex3 (x - w/2.0) (y - h/2.0) 0.0,
                Vertex3 (x - w/2.0) (y + h/2.0) 0.0,
                Vertex3 (x + w/2.0) (y + h/2.0) (0.0 :: GLdouble) ]
renderGameObject (ABlock{center=cn, width=w, height=h}) = do -- 破壊不可能ブロックの描画
  let (x, y) = cn
  preservingMatrix $ do
    translate (Vector3 x y 0 :: Vector3 GLdouble)
    scale w h 1.0
    renderObject Solid (Cube 1.0)
renderGameObject (Paddle{center=cn, width=w, height=h}) = do -- パドルの描画
  let (x, y) = cn
  renderPrimitive LineStrip $ mapM_ vertex [
                Vertex3 (x + w/2.0) (y + h/2.0) 0.0,
                Vertex3 (x + w/2.0) (y - h/2.0) 0.0,
                Vertex3 (x - w/2.0) (y - h/2.0) 0.0,
                Vertex3 (x - w/2.0) (y + h/2.0) 0.0,
                Vertex3 (x + w/2.0) (y + h/2.0) (0.0 :: GLdouble) ]
renderGameObject (Box{center=cn, width=w, height=h}) = do
  let (x, y) = cn
  renderPrimitive LineStrip $ mapM_ vertex [
                Vertex3 (x + w/2.0) (y + h/2.0) 0.0,
                Vertex3 (x + w/2.0) (y - h/2.0) 0.0,
                Vertex3 (x - w/2.0) (y - h/2.0) 0.0,
                Vertex3 (x - w/2.0) (y + h/2.0) 0.0,
                Vertex3 (x + w/2.0) (y + h/2.0) (0.0 :: GLdouble) ]

main :: IO()
main = do
  keystate <- newIORef []
  gameObjects <- newIORef ([ Ball (0.0, (-12.0)) (0.4, 0.4),
                             Box (0.0, 0.0) 30 60,
                             Paddle (0.0, (-13.0)) 8.0 1.0 (0.0, 0.0)]
                           ++ [ Block (x, y) 2.0 1.0 | x <- [(-12.0), (-10.0) .. 12.0], y <- [3.0,4.0..28.0]])
                 
  initialDisplayMode $= [RGBAMode, DoubleBuffered, WithDepthBuffer]
  initialWindowSize $= Size 640 480
  getArgsAndInitialize
  -- 窓の生成
  createWindow "Reflect Ball"
  -- 描画コールバックの登録
  keyboardMouseCallback $= Just (keyboardProc keystate)
  displayCallback $= (display gameObjects keystate)
  -- ウィンドウサイズが変更された時に呼ぶコールバック関数の指定
  reshapeCallback $= Just reshape
  -- タイマー関数の登録
  addTimerCallback timerInterval $ timer (display gameObjects keystate)
  -- GLUTのメインループへ
  mainLoop

display gameObjects keystate = do
  ks <- readIORef keystate
  modifyIORef gameObjects (update ks)
  -- 背景を黒で塗り潰し
  clearColor $= Color4 1.0 1.0 1.0 0.0
  clear [ColorBuffer, DepthBuffer] 
  -- 単位行列の読み込み
  loadIdentity
  -- 表示
  gs <- readIORef gameObjects
  mapM_ renderGameObject gs
  swapBuffers

timer act = do
  act
  addTimerCallback timerInterval $ timer act

reshape size@(Size w h) = do
  viewport $= (Position 0 0, size)
  matrixMode $= Projection
  loadIdentity
  -- fov アスペクト比 ニアクリップ ファークリップ
  perspective 80 (fromIntegral w / fromIntegral h) 0.1 200.0
  -- 視点 注視点 カメラの上方向
  lookAt (Vertex3 0.0 0.0 40.0) (Vertex3 0.0 0.0 0.0) (Vector3 0.0 1.0 0.0)
  matrixMode $= Modelview 0

  -- ライト設定
  lighting $= Enabled
  -- ディフューズ
  position (Light 0) $= Vertex4 0.0 (-100.0) 100.0 1.0
  diffuse (Light 0) $= Color4 0.0 0.0 1.0 1.0
  light (Light 0) $= Enabled
  materialDiffuse FrontAndBack $= Color4 1.0 1.0 1.0 1.0

  -- これをしないとDepthバッファが有効にならない
  depthFunc $= Just Less

keyboardProc keystate ch state _ pos
    | ch     == Char 'q' = exitWith ExitSuccess        --qが押されたら終了
    | state  == Down     = modifyIORef keystate (nub.(++[ch]))
    | state  == Up       = modifyIORef keystate (filter (/=ch))
    | otherwise          = return ()