Mô hình các kiểu dữ liệu trong trò chơi

Một phần của tài liệu (LUẬN văn THẠC sĩ) nghiên cứu lập trình phản ứng với ngôn ngữ ELM (Trang 57)

Direction Left Right Down Up State Play Pause

3.3 Xây dựng code chƣơng trình với ngôn ngữ Elm

Cũng giống nhƣ các ngôn ngữ lập trình thông dụng để bắt đầu lập trình game chúng ta sẽ kết nối tới các thƣ viện cần dùng. Mỗi thƣ viện đều có các chức năng riêng phục vụ việc tạo giao diện và xử lý các sự kiện ngƣời dùng tác động vào.

1 import Color (..)

2 import Graphics.Collage (..)

3 import Graphics.Element (..)

4 import Signal (Signal, map, map2, foldp, sampleOn, (~), (<~))

5 import Mouse 6 import Window 7 import Time (..)

8 import Text exposing (Text)

9 import Keyboard 10 import Debug 11 import Char

3.3.1 Xây dựng giao diện

Đầu tiên ta sẽ phân tích và xây dựng hàm view hàm sẽ hiển thị giao diện ngƣời dùng, hàm đƣợc khai báo với 2 tham số đó là: Một cặp số nguyên là giá trị chiều rộng và cao của cửa sổ màn hình, tham số thứ 2 là các đối tƣợng và trạng thái của trò chơi.

180 view : (Int,Int) -> Data -> Element

181 view (w,h) ({ball,img1,img2,state} as game) = 182 let

...

203 in ...

Với biểu thức let phía bên trong giúp ngƣời lập trình có thể định nghĩa các hàm và thực thi giá trị của chúng. Biểu thức in sẽ hiển thị tất cả các thành phần đồ họa và xử lý, cập nhật các giá trị tính toán trong chƣơng trình.

Các yếu tố đồ họa của trò chơi gồm: Một sân bóng bằng cách lấy một ảnh nền từ trong thƣ mục đƣờng dẫn chứa ảnh đó.

207 toForm (image 960 500 "image/Ground.jpg")

Sau đó ta tạo các đối tƣợng của trò chơi bao gồm: 2 ngƣời chơi và một quả bóng.

209 , make red ball (oval 20 20)

210 , toForm playerImage1 211 |> move(img1.x,img1.y)

212 , toForm playerImage2 213 |> move(img2.x,img2.y)

Ta cùng xét lại đoạn mã trên, việc tạo ra quả bóng ta dùng hàm make, chức năng hàm này sẽ giúp tạo các đối tƣợng dạng hình khối (shape), tô mầu và di chuyển nó theo tín hiệu đầu vào.

263 make color obj shape =

264 shape

265 |> filled color

266 |> move (obj.x, obj.y)

Với 2 cầu thủ ta không dùng hàm make để tạo, chúng sẽ tải ảnh theo đƣờng dẫn làm cho hình ảnh cầu thủ di chuyển động hơn và mƣợt mà hơn.

184 verb1 = if (img1.vx == 0 && img1.vy == 0) || (img2.vx == 0 &&

img2.vy == 0) then "stand" else "walk"

187 dir1 = case img1.dir of 188 Right -> "right12.gif" 189 Left -> "left1.png" 190 Down -> "down1.png" 191 Up -> "up1.gif" 192 dir2 = case img2.dir of 193 Right -> "right2.gif" 194 Left -> "left2.gif" 195 Down -> "down2.png" 196 Up -> "up2.gif"

198 src1 = "image/"++ verb1 ++ "/" ++ dir1 199 src2 = "image/"++ verb1 ++ "/" ++ dir2 201 playerImage1 = image 80 80 src1

202 playerImage2 = image 80 80 src2

Tiếp theo chúng ta sẽ xây dựng 2 khung thành. 216 ,filled white (rect goal.w goal.h)

217 |> move(-halfWidth + goal.w/2, 0)

218 ,filled white (rect goal.w goal.h)

219 |> move(halfWidth - goal.w/2, 0)

Chiều rộng và chiều cao của sân bóng và chiều rộng và chiều cao của khung thành đƣợc gắn giá trị. 175 (gameWidth,gameHeight) = (800,400) 176 (halfWidth,halfHeight) = (400,200) 177 goal = {w = 1, h =120} 178 gameOver = 5

Cuối cùng là các dòng text sẽ đƣợc in ra để thông báo hƣớng dẫn chơi và bảng điểm (score) của đội chơi. Hàm txt1 sẽ hiển thị một dòng văn bản ra màn hình.

251 txt1 : (Text.Text -> Text.Text) -> String -> Element 252 txt1 string = 253 Text.fromString string 254 |> Text.color black 255 |> Text.height 20 256 |> Text.bold 257 |> Text.monospace 258 |> leftAligned

260 msg = " SPACE to start, N to Restartgame, Hình 3.4: Giao diện chính của trò chơi

261 &uarr; &darr; &larr; &rarr; and wasd to move,

262 Enter to shoot of player1 and Shift to shoot of player2"

Các dòng văn bản hƣớng dẫn sẽ đƣợc in lên một ảnh nền đƣợc tải từ trong thƣ mục ảnh vào.

224 , toForm (image 960 96 "image/intruction2.gif")

225 |> move (0,-gameHeight/2 - 90)

227 , toForm (if state == Play then spacer 1 1 else txt1 msg)

228 |> move (150,-gameHeight/2 - 90)

Ngoài việc dùng hàm để hiện thị văn bản, ngƣời lập trình vẫn có thể hiển thị văn bản ra ngoài màn hình bằng cú pháp.

235 toForm (centered (

236 Text.fromString (if (img1.score < gameOver && img2.score <

gameOver) then "" else "Game Over!")

237 |> Text.color red 238 |> Text.bold 239 |> Text.height 68

240 ))

Dòng lệnh trên sẽ hiển thị chữ “Game Over!” ra ngoài màn hình khi trò chơi kết thúc.

3.3.2 Xây dựng mô hình dữ liệu

Ở phần này chúng tôi xây dựng các kiểu dữ liệu của chƣơng trình. Để khai báo một kiểu dữ liệu mới ta dùng từ khóa type aliaslà khai báo kiểu bản ghi. Một bản ghi có thể chứa nhiều kiểu giá trị khác nhau. Sau đây là các kiểu dữ liệu trong chƣơng trình.

14 type alias Data = {state: State, ball : Ball, img1 : Player, img2 : Player}

Kiểu Data chứa tất cả các đối tƣợng và trạng thái của chƣơng trình. Các đối tƣợng ở đây là quả bóng và 2 cầu thủ, ngoài ra còn có kiểu trạng thái của trò chơi. 15 type alias Keys = {x: Int, y: Int}

Kiểu Keys khai báo tín hiệu đƣợc nhập vào từ bàn phím của các phím mũi tên và các phím ( w, a, s, d ) dùng để điều khiển hƣớng di chuyển của đối tƣợng. 17 type alias Ball = { x: Float, y: Float, vx: Float, vy: Float, shoot1: Bool, shoot2: Bool }

Kiểu Ball sẽ lƣu các giá trị của quả bóng nhƣ vị trí tọa độ, vận tốc bóng và thuộc tính để xác định xem bóng đang thuộc cầu thủ nào: shoot1, shoot2

18 type alias Player = { x: Float, y: Float, vx: Float, vy: Float,

dir: Direction, score: Int}

Kiểu Player lƣu các giá trị thuộc tính của cầu thủ đó là vị trí tọa độ, vận tốc di chuyển, phƣơng hƣớng di chuyển và số điểm của từng đội chơi.

20 type alias Input = { isDown: Bool, space: Bool, enter: Bool, shift: Bool, dir1: Keys, dir2: Keys, delta: Time}

Kiểu Input chứa các tín hiệu đầu vào của chƣơng trình. Tín hiệu space dạng True hoặc False để bắt đầu phát bóng, tín hiệu isDown dạng True hoặc False dùng để khởi động lại trò chơi khi trò chơi kết thúc, chức năng của từ khóa này là có thể nhập bất kỳ phím nào trên bàn phím. Trong chƣơng trình này là phím “N” để khởi động lại trò chơi: Keyboard.isDown (Char.toCode 'N')

Tín hiệu enter là dùng để sút bóng nếu cầu thủ đội A giữ bóng. Tín hiệu shift là cầu thủ đội B sút bóng. Tín hiệu dir1 là tín hiệu từ bàn phím chỉ hƣớng di chuyển của các cầu thủ 1, tín hiệu dir2 là hƣớng di chuyển của cầu thủ 2. Tín hiệu delta là tín hiệu thời gian chức năng làm cho các đối tƣợng trong chƣơng trình di chuyển mƣợt mà và động hơn.

22 type State = Play | Pause

23 type Direction = Left | Right | Down | Up

Kiểu State và Direction là kiểu dữ liệu có giá trị mặc định. Kiểu State có 2 trạng thái ngừng chơi và tiếp tục chơi. Kiểu Direction có 4 trạng thái di chuyển sang trái, phải, lên trên và xuống dƣới.

25 defaultGame : Data 26 defaultGame = { 27 state = Pause,

28 ball = Ball 0 0 200 200 False False,

29 img1 = Player 200 0 0 0 Left 0,

30 img2 = Player -200 0 0 0 Right 0

31 }

Hàm defaultGame dùng để khởi tạo các giá trị ban đầu của các đối tƣợng trong trò chơi và trạng thái ngƣời chơi. Các giá trị này sẽ đƣợc cập nhật liên tục trong quá trình chơi nếu nhƣ có sự kiện xảy ra.

3.3.3 Xây dựng tín hiệu đầu vào

Trong chƣơng trình hàm thời gian sẽ đƣợc định nghĩa bởi hàm fps: 169 fps : number -> Signal Time

170 delta = Signal.map inSeconds (fps 35)

Hàm sẽ thực hiện một số lƣợng khung hình trong mỗi giây và kết quả tín hiệu sẽ đƣợc trả lại nhanh nhất có thể. Chức năng của nó làm cho các đối tƣợng di chuyển mƣợt và động hơn.

Tín hiệu đầu vào Input gồm phím Space, phím Enter, phím shift, phím “N”, các phím mũi tên (Arrows), các phím (w, a, s, d) và cả tín hiệu thời gian (Time). Để nối tất cả chúng thành một dòng tín hiệu chung ta sử dụng hàm (<~, ~) các hàm này tƣơng đƣơng với hàm map.

158 input : Signal Input

159 input = sampleOn delta <| (Input

160 <~ Keyboard.isDown (Char.toCode 'N') 161 ~ Keyboard.space 162 ~ Keyboard.enter 163 ~ Keyboard.shift 164 ~ Keyboard.arrows 165 ~ Keyboard.wasd 166 ~ delta)

Hàm sampleOn nhận hai tín hiệu đầu vào và trả lại tín hiệu sau dựa trên tín hiệu trƣớc đó. Ở đoạn mã trên giá trị trả về là một trong các tín hiệu của dòng dựa trên giá trị thời gian.

3.3.4 Cập nhật dữ liệu chương trình

Một chƣơng trình game sẽ có các đối tƣợng tƣơng tác với nhau vì vậy ta sẽ viết trƣớc các hàm xử lý các sự kiện khi chúng va chạm với nhau.

78 near : number -> number -> number -> Bool 79 near k c n =

80 n >= k-c && n <= k+c

Hàm near là hàm kiểm tra khoảng cách giữa các đối tƣợng xem chúng có gần nhau hay không và nếu gần nhau thì sẽ thực thị một sự kiện nào đó.

82 Within : Ball -> Player -> Bool 83 within ball player =

84 near player.x 40 ball.x && near player.y 40 ball.y

Hàm within kiểm tra sự đụng độ xem quả bóng có gần cầu thủ không. 72 stepv : Float -> Bool -> Bool -> Float

73 stepv v bottom up =

74 if | bottom -> abs v 75 | up -> -(abs v)

76 | otherwise -> v

Hàm stepv thay đổi vận tốc thông qua sự kiện va chạm giữa bóng với các cạnh biên của sân bóng. Nếu bóng va trạm với biên trên thì sẽ thay đổi với hàm trị tuyệt đối (abs v) và ngƣợc lại nếu va chạm biên dƣới thì vận tốc sẽ có giá trị phủ định với hàm -(abs v). Nếu không có sự va chạm bóng sẽ giữ vận tốc di chuyển mà không có sự thay đổi. Quá trình cập nhật vị trí và vận tốc quả bóng sẽ đƣợc lập đi lập lại. 118 distance t ({x,y,vx,vy} as obj) = 119 { obj | 120 x <- x + vx * t, 121 y <- y + vy * t 122 }

Hàm distance cập nhật khoảng cách di chuyển của các đối tƣợng với một khoảng thời gian t. Vị trí mới của đối tƣợng sẽ đƣợc cập nhật bằng việc tọa độ cũ cộng với vận tốc nhân với thời gian.

Tiếp theo ta xét hàm updateBall cập nhật dữ liệu của quả bóng khi di chuyển và phản ứng với các đối tƣợng trong chƣơng trình.

86 updateBall : Time -> Ball -> Player -> Player -> Ball 87 updateBall t ({x,y,vx,vy} as ball) p1 p2 =

88 if (ball `within` p1) && ball.shoot1 == False then 89 {ball |

90 vx <- p1.vx, 91 vy <- p1.vy,

92 x <- p1.x, y <- p1.y, shoot2 <- False}

93 else if (ball `within` p2) && ball.shoot2 == False then 94 {ball |

96 vy <- p2.vy,

97 x <- p2.x, y <- p2.y, shoot1 <- False}

98 else if not (ball.x |> near 0 halfWidth) then

99 {ball | x <- 0, y <- 0, shoot1 <- False, shoot2 <- False}

Khi bóng di chuyển gập cầu thủ 1 nó và trạng thái shoot1 == False tức là bóng sẽ không đƣợc sút và bóng sẽ di chuyển theo cầu thủ đó. Tƣơng tự với cầu thủ 2 cũng vậy. Nhƣng nếu bóng không nằm trong khoảng ½ sân bóng (halfWidth) thì tọa độ quả bóng sẽ quay về vị trí (0, 0) việc kiểm tra khoảng cách này thực hiện bằng hàm near. Nếu quả bóng nằm trong sân thì vị trí tọa độ và vận tốc quả bóng sẽ đƣợc cập nhật. Xét quá trình cập nhật vận tốc vx có điều kiện (y > goal.h/2 || y < -goal.h/2) thì giá trị của quả bóng sẽ đƣợc cập nhật tiếp, ngƣợc lại quả bóng sẽ bay vào khung thành và quay lại vị trí (0, 0).

Xét tiếp quá trình hoạt động của quả bóng. 100 else if ball.shoot1 == True then 101 distance t

102 { ball |

103 vx <- stepv vx (x < 20 - halfWidth && (y > goal.h/2 || y < -

goal.h/2)) True,

104 vy <- stepv vy (y < 20 - halfHeight) (y > halfHeight - 20),

105 shoot1 <- if (x <= -halfWidth + 20 && (y > goal.h/2 || y < -

goal.h/2)) || (x >= halfWidth - 20 && (y > goal.h/2 || y < -goal.h/2))

then False else True,

106 shoot2 <- False 107 }

108 else if ball.shoot2 == True then distance t 109 { ball |

110 vx <- stepv vx (x < 20 - halfWidth && (y > goal.h/2 || y < -

goal.h/2)) False,

112 vy <- stepv vy (y < 20 - halfHeight) (y > halfHeight - 20),

113 shoot1 <- False,

114 shoot2 <- if (x <= -halfWidth + 20 && (y > goal.h/2 || y < -

goal.h/2)) || (x >= halfWidth - 20 && (y > goal.h/2 || y < -goal.h/2))

then False else True

115 }

Nếu nhƣ quà bóng đƣợc sút bởi cầu thủ ball.shoot1 == True hoặc ball.shoot2 == True thì tọa độ và vận tốc bóng lại đƣợc thay đổi.

116 else distance t 117 { ball |

118 vx <- stepv vx (x <= 20 - halfWidth && (y > goal.h/2 || y < -goal.h/2)) (x >= halfWidth - 20 && (y > goal.h/2 || y < -goal.h/2)),

119 vy <- stepv vy (y < 20 - halfHeight) (y > halfHeight - 20)

120 }

Nếu nhƣ bóng không chạm cầu thủ nào nó sẽ thay đổi vận tốc vx, vy nếu trạm vào các cạnh biên của sân và giữ nguyên vận tốc nếu không chạm vào đâu. Nếu bóng rơi vào tọa độ (y < goal.h/2 || y > -goal.h/2) thì bóng sẽ nằm trong khung thành và đƣợc cập nhật về tọa đô (0, 0).

Xét việc di chuyển của cầu thủ 2 đội chơi thông qua hàm updatePlayer. Hàm này đƣợc khai báo với các tham số nhƣ thời gian (Time), phím nhập vào từ bàn phím (keys), điểm của ngƣời chơi (points) và ngƣời chơi đang cầm bóng ( currentPlayer) và cầu thủ còn lại.

124 updatePlayer: Time -> Keys -> Int -> Player -> Player -> Player 125 updatePlayer t keys points currentPlayer player

Khi 2 cầu thủ di chuyển mà va chạm với nhau thì sẽ không cho vƣợt qua và bắt buộc phải di chuyển hƣớng khác. Quá trình này sẽ đƣợc kiểm tra bằng hàm near.

127 if (near currentPlayer.x 40 player.x && near currentPlayer.y 40

player.y )then

128 distance t {currentPlayer|

129 x <- if | (currentPlayer.dir == Left && keys.x < 0) ->

currentPlayer.x + 40

130 | (currentPlayer.dir == Right && keys.x > 0) ->

currentPlayer.x - 40

131 | otherwise -> currentPlayer.x,

132 y <- if | (currentPlayer.dir == Up && keys.y > 0) ->

currentPlayer.y - 40

133 | (currentPlayer.dir == Down && keys.y < 0) ->

currentPlayer.y + 40

134 | otherwise -> currentPlayer.y 135 }

Hàm near sẽ kiểm tra xem nếu hai cầu thủ cách nhau một khoảng x = 40 và y = 40 thì ngƣời chơi sẽ bị đẩy văng ra một khoảng là 40. Còn nếu không có va

chạm thì các cầu thủ sẽ đƣợc cập nhật tọa độ và khoảng cách, hƣớng di chuyển nhƣ đoạn mã dƣới đây.

139 else 140 distance t { currentPlayer| 141 vx <- toFloat (keys.x)*200, 142 vy <- toFloat (keys.y)*200, 143 dir <- 144 if | keys.x > 0 -> Right 145 | keys.x < 0 -> Left 146 | keys.y < 0 -> Down 147 | keys.y > 0 -> Up

148 | otherwise -> currentPlayer.dir 149 }

Trong biểu thức inta có hàm clamp giới hạn tọa độ của cầu thủ với biên của sân sân bóng và không cho ra khỏi sân bóng. Việc cập nhật điểm của đội chơi cũng sẽ đƣợc cập nhật nếu nhƣ cầu thủ của đội nào ghi bàn thắng.

151 { p |

152 x <- clamp (20 - halfWidth) (halfWidth - 20) p.x,

153 y <- clamp (20 - halfHeight) (halfHeight - 20) p.y,

154 score <- currentPlayer.score + points 155 }

Sau khi viết xong các hàm cập nhật các đối tƣợng ta sẽ gộp tất cả lại thành một hàm cập nhật dữ liệu chung gồm cả các tín hiệu đầu vào từ bàn phím. Hàm update sẽ bao gồm các tính hiệu đầu vào, các đối tƣợng và cả trạng thái của trò chơi.

33 update: Input -> Data -> Data

34 update{isDown,space,enter,shift,dir1,dir2,delta} ({state,ball,img1,img2} as game) =

35 let 36 score1 =

37 if ball.x > halfWidth then 1 else 0

38 score2 =

39 if ball.x < -halfWidth then 1 else 0

41 newState =

43 if img1.score >= gameOver || img2.score >= gameOver then Pause

44 else Play

45 | score1 /= score2 -> Pause 46 | otherwise -> state

48 newBall =

49 if state == Pause then 50 ball

51 else updateBall delta ball img1 img2

Trong hàm ta tiếp tục sử dụng biểu thức let để định nghĩa hàm và giá trị của

Một phần của tài liệu (LUẬN văn THẠC sĩ) nghiên cứu lập trình phản ứng với ngôn ngữ ELM (Trang 57)

Tải bản đầy đủ (PDF)

(79 trang)