Blog Post

Small Basic Blog
14 MIN READ

Small Basic Game Programming - Air Hockey Game

NonkiTakahashi's avatar
NonkiTakahashi
Iron Contributor
Feb 12, 2019
First published on MSDN on Aug 25, 2014

Authored by Nonki Takahashi


Both in Challenge of the Month - July 2013 and Challenge of the Month - July 2014 , the challenges of air hockey game was presented.  Through this challenges, I wrote KLB414-4 for an year.  [W] key and [S] key are for the player 1 (left side) to move the mallet, [O] key and [L] key are for the player 2 (right side).  A winner is the first to win 7 points.



Main


Main loop repeats games.  In this program, the value of the variable continue is always "True".



1. ' Air Hockey 0.5b

2. ' Copyright (c) 2013-2014 Nonki Takahashi. MIT License.

3. '

4. ' History:

5. ' 0.5b 2014-07-09 Determined a winner is the first to win 7 points. (KLB414-4)

6. ' 0.4b 2014-07-09 Supported collision between puck and mallet. (KLB414-3)

7. ' 0.3a 2014-07-03 Added mallet control. (KLB414-2)

8. ' 0.2a 2013-07-30 Changed field design. (KLB414-1)

9. ' 0.11a 2013-07-29 Modified for Silverlight. (KLB414-0)

10. ' 0.1a 2013-07-29 Created as alpha version. (KLB414)

11. '

12. ' Reference:

13. ' LitDev, Small Basic: Dynamic Graphics, TechNet Wiki, 2013-2014.

14. '

15. GraphicsWindow . Title = "Air Hockey 0.5b - W,S for P1; O,L for P2"

16. gw = 598

17. gh = 428

18. GraphicsWindow . Width = gw

19. GraphicsWindow . Height = gh

20. GraphicsWindow . BackgroundColor = "DimGray"

21. Field_Init ( )

22. continue = "True"

23. While continue

24. Game_Init ( )

25. Game_Start ( )

26. Game_End ( )

27. EndWhile


Field Initialization


This subroutine draws an air hockey field.  And adds a puck and mallets for both players as Shapes.



28. Sub Field_Init

29. fh = 30 ' font height

30. GraphicsWindow . FontName = "Trebuchet MS"

31. GraphicsWindow . BrushColor = "White"

32. GraphicsWindow . FontSize = fh

33. score [ 1 ] [ "obj" ] = Shapes . AddText ( 0 )

34. Shapes . Move ( score [ 1 ] [ "obj" ] , gw / 2 - 100 , 10 )

35. score [ 2 ] [ "obj" ] = Shapes . AddText ( 0 )

36. Shapes . Move ( score [ 2 ] [ "obj" ] , gw / 2 + 100 , 10 )

37. field [ "width" ] = 580

38. field [ "height" ] = 360

39. field [ "x" ] = ( gw - field [ "width" ] ) / 2

40. field [ "y" ] = ( gh - field [ "height" ] + fh ) / 2

41. field [ "x2" ] = field [ "x" ] + field [ "width" ]

42. field [ "y2" ] = field [ "y" ] + field [ "height" ]

43. param [ "x" ] = field [ "x" ] - 10

44. param [ "y" ] = ( field [ "y" ] + field [ "y2" ] ) / 2 - 70

45. param [ "width" ] = 20

46. param [ "height" ] = 140

47. param [ "border-radius" ] = 10

48. goal [ "y" ] = param [ "y" ]

49. goal [ "y2" ] = param [ "y" ] + param [ "height" ]

50. GraphicsWindow . BrushColor = "Black"

51. FillRoundRectangle ( )

52. param [ "x" ] = field [ "x2" ] - 10

53. FillRoundRectangle ( )

54. GraphicsWindow . BrushColor = "Blue"

55. GraphicsWindow . FillRectangle ( field [ "x" ] , field [ "y" ] , field [ "width" ] , field [ "height" ] )

56. GraphicsWindow . PenWidth = 5

57. GraphicsWindow . PenColor = "LightGray"

58. param [ "x" ] = field [ "x" ] + 20

59. param [ "y" ] = field [ "y" ] + 20

60. param [ "width" ] = field [ "width" ] - 40

61. param [ "height" ] = field [ "height" ] - 40

62. param [ "border-radius" ] = 100

63. DrawRoundRectangle ( )

64. x = param [ "x" ] + param [ "width" ] / 2

65. GraphicsWindow . DrawLine ( x , param [ "y" ] , x , param [ "y" ] + param [ "height" ] )

66. GraphicsWindow . BrushColor = "Black"

67. GraphicsWindow . PenWidth = 0

68. For y = field [ "y" ] + 20 To field [ "y" ] + field [ "height" ] - 20 Step 20

69. For x = field [ "x" ] + 20 To field [ "x" ] + field [ "width" ] - 20 Step 20

70. GraphicsWindow . FillEllipse ( x - 1 , y - 1 , 2 , 2 )

71. EndFor

72. EndFor

73. GraphicsWindow . BrushColor = "Yellow"

74. puck [ "size" ] = 34

75. puck [ "r" ] = puck [ "size" ] / 2

76. puck [ "obj" ] = Shapes . AddEllipse ( puck [ "size" ] , puck [ "size" ] )

77. GraphicsWindow . BrushColor = "White"

78. mallet [ 1 ] [ "size" ] = 34

79. mallet [ 1 ] [ "r" ] = mallet [ 1 ] [ "size" ] / 2

80. mallet [ 1 ] [ "obj" ] = Shapes . AddEllipse ( mallet [ 1 ] [ "size" ] , mallet [ 1 ] [ "size" ] )

81. mallet [ 2 ] [ "size" ] = 34

82. mallet [ 2 ] [ "r" ] = mallet [ 2 ] [ "size" ] / 2

83. mallet [ 2 ] [ "obj" ] = Shapes . AddEllipse ( mallet [ 2 ] [ "size" ] , mallet [ 2 ] [ "size" ] )

84. GraphicsWindow . BrushColor = "DimGray"

85. screen = Shapes . AddRectangle ( gw , gh )

86. Shapes . SetOpacity ( screen , 0 )

87. EndSub


Game Initialization


This subroutine initializes the position and the velocity of the puck and the positions of the mallets.



88. Sub Game_Init

89. x = field [ "x" ]

90. y = field [ "y" ] + field [ "height" ] / 2

91. mallet [ 1 ] [ "cx" ] = x + 20

92. mallet [ 1 ] [ "cy" ] = y

93. Shapes . Move ( mallet [ 1 ] [ "obj" ] , mallet [ 1 ] [ "cx" ] - mallet [ 1 ] [ "r" ] , mallet [ 1 ] [ "cy" ] - mallet [ 1 ] [ "r" ] )

94. mallet [ 2 ] [ "cx" ] = x + field [ "width" ] - 20

95. mallet [ 2 ] [ "cy" ] = y

96. Shapes . Move ( mallet [ 2 ] [ "obj" ] , mallet [ 2 ] [ "cx" ] - mallet [ 2 ] [ "r" ] , mallet [ 2 ] [ "cy" ] - mallet [ 2 ] [ "r" ] )

97. puck [ "cx" ] = x + ( field [ "width" ] / 2 )

98. puck [ "cy" ] = y

99. Shapes . Move ( puck [ "obj" ] , puck [ "cx" ] - puck [ "r" ] , puck [ "cy" ] - puck [ "r" ] )

100. v0 = 400

101. puck [ "vx" ] = 100

102. puck [ "vy" ] = 80

103. AdjustV0 ( )

104. score [ 1 ] [ "value" ] = 0

105. Shapes . SetText ( score [ 1 ] [ "obj" ] , score [ 1 ] [ "value" ] )

106. score [ 2 ] [ "value" ] = 0

107. Shapes . SetText ( score [ 2 ] [ "obj" ] , score [ 2 ] [ "value" ] )

108. deltaY = puck [ "size" ]

109. GraphicsWindow . KeyDown = OnKeyDown

110. EndSub


Playing the Game


This subroutine continues the game until one player wins 7 points.  In this loop, the position and the velocity of the puck is updated 24 times per one second.



111. Sub Game_Start

112. inGame = "True"

113. dt = 1 / 24 ' [second]

114. While inGame

115. start = Clock . ElapsedMilliseconds

116. UpdatePuck ( )

117. delay = dt * 1000 - ( Clock . ElapsedMilliseconds - start )

118. If 0 < delay Then

119. Program . Delay ( delay )

120. EndIf

121. EndWhile

122. EndSub


Game End


This subroutine shows the winner.



123. Sub Game_End

124. Shapes . SetOpacity ( screen , 40 )

125. GraphicsWindow . FontSize = 40

126. GraphicsWindow . BrushColor = "White"

127. result = Shapes . AddText ( "PLAYER " + winner + " WON" )

128. x = ( gw - 283 ) / 2

129. y = ( gh - 40 ) / 2

130. Shapes . Move ( result , x , y )

131. Sound . PlayBellRingAndWait ( )

132. Program . Delay ( 5000 )

133. Shapes . SetOpacity ( screen , 0 )

134. Shapes . Remove ( result )

135. EndSub


Adjusting the Velocity of the Puck


This subroutine adjusts the scalar value of the velocity for the puck - puck["vx"] and puck["vy"] to be the same value of the variable v0.



136. Sub AdjustV0

137. v = Math . SquareRoot ( Math . Power ( puck [ "vx" ] , 2 ) + Math . Power ( puck [ "vy" ] , 2 ) )

138. puck [ "vx" ] = puck [ "vx" ] * v0 / v

139. puck [ "vy" ] = puck [ "vy" ] * v0 / v

140. EndSub


Collision Detection


This subroutine detects collision between the puck and the mallets, and updates the velocity of the puck if the collision is detected.  The origin of this subroutine is the same named subroutine written in a TechNet Wiki article Dynamic Graphics by LitDev.  The original is for collision between balls, but this subroutine is changed that only the puck is reflected because the mallets are held by the players.



141. Sub CollisionCheck

142. For i = 1 To 2

143. dx = mallet [ i ] [ "cx" ] - puck [ "cx" ]

144. dy = mallet [ i ] [ "cy" ] - puck [ "cy" ]

145. distance = Math . SquareRoot ( dx * dx + dy * dy )

146. If distance < puck [ "size" ] Then

147. Sound . PlayClick ( )

148. relativeVx = puck [ "vx" ]

149. relativeVy = puck [ "vy" ]

150. nx = dx / distance

151. ny = dy / distance

152. l = nx * relativeVx + ny * relativeVy

153. relativeVx = relativeVx - ( 2 * l * nx )

154. relativeVy = relativeVy - ( 2 * l * ny )

155. puck [ "vx" ] = relativeVx

156. puck [ "vy" ] = relativeVy

157. puck [ "cx" ] = puck [ "cx" ] - nx * ( puck [ "size" ] - distance )

158. puck [ "cy" ] = puck [ "cy" ] - ny * ( puck [ "size" ] - distance )

159. EndIf

160. EndFor

161. EndSub


Key Input Event Handler


This subroutine moves the mallets when the keys are input.



162. Sub OnKeyDown

163. key = GraphicsWindow . LastKey

164. If key = "W" Then ' player 1 up

165. If goal [ "y" ] < = mallet [ 1 ] [ "cy" ] - deltaY Then

166. mallet [ 1 ] [ "cy" ] = mallet [ 1 ] [ "cy" ] - deltaY

167. Shapes . Move ( mallet [ 1 ] [ "obj" ] , mallet [ 1 ] [ "cx" ] - mallet [ 1 ] [ "r" ] , mallet [ 1 ] [ "cy" ] - mallet [ 1 ] [ "r" ] )

168. EndIf

169. ElseIf key = "S" Then ' player 1 down

170. If mallet [ 1 ] [ "cy" ] + deltaY < = goal [ "y2" ] Then

171. mallet [ 1 ] [ "cy" ] = mallet [ 1 ] [ "cy" ] + deltaY

172. Shapes . Move ( mallet [ 1 ] [ "obj" ] , mallet [ 1 ] [ "cx" ] - mallet [ 1 ] [ "r" ] , mallet [ 1 ] [ "cy" ] - mallet [ 1 ] [ "r" ] )

173. EndIf

174. ElseIf key = "O" Then ' player 2 up

175. If goal [ "y" ] < = mallet [ 2 ] [ "cy" ] - deltaY Then

176. mallet [ 2 ] [ "cy" ] = mallet [ 2 ] [ "cy" ] - deltaY

177. Shapes . Move ( mallet [ 2 ] [ "obj" ] , mallet [ 2 ] [ "cx" ] - mallet [ 2 ] [ "r" ] , mallet [ 2 ] [ "cy" ] - mallet [ 2 ] [ "r" ] )

178. EndIf

179. ElseIf key = "L" Then ' player 2 down

180. If mallet [ 2 ] [ "cy" ] + deltaY < = goal [ "y2" ] Then

181. mallet [ 2 ] [ "cy" ] = mallet [ 2 ] [ "cy" ] + deltaY

182. Shapes . Move ( mallet [ 2 ] [ "obj" ] , mallet [ 2 ] [ "cx" ] - mallet [ 2 ] [ "r" ] , mallet [ 2 ] [ "cy" ] - mallet [ 2 ] [ "r" ] )

183. EndIf

184. EndIf

185. EndSub


Update of the Velocity and the Position for the Puck


This subroutine simulates the movement of the puck which moves with the constant velocity along with Newton's law of inertia.  And it also checks goal or collision with the frame of the field.  If a player get a goal, the position of the puck is set back to the center.  At the last, it also checks collision with mullets by calling CallingCheck().  If a player get 7 points, inGame flag is set as "False" to terminate the loop in Game_Start().



186. Sub UpdatePuck

187. isGoal = "False"

188. x = puck [ "cx" ] + dt * puck [ "vx" ]

189. If x < field [ "x" ] + puck [ "r" ] Then

190. y = puck [ "cy" ] + dt * ( field [ "x" ] - puck [ "cx" ] ) * puck [ "vy" ] / puck [ "vx" ]

191. If ( goal [ "y" ] < y ) And ( y < goal [ "y2" ] ) Then

192. score [ 2 ] [ "value" ] = score [ 2 ] [ "value" ] + 1

193. Shapes . SetText ( score [ 2 ] [ "obj" ] , score [ 2 ] [ "value" ] )

194. isGoal = "True"

195. If score [ 2 ] [ "value" ] = 7 Then

196. inGame = "False"

197. winner = 2

198. EndIf

199. Else

200. puck [ "cx" ] = field [ "x" ] + puck [ "r" ] + ( field [ "x" ] + puck [ "r" ] - x )

201. puck [ "vx" ] = - puck [ "vx" ]

202. Sound . PlayClick ( )

203. EndIf

204. ElseIf field [ "x2" ] - puck [ "r" ] < x Then

205. y = puck [ "cy" ] + dt * ( field [ "x2" ] - puck [ "cx" ] ) * puck [ "vy" ] / puck [ "vx" ]

206. If ( goal [ "y" ] < y ) And ( y < goal [ "y2" ] ) Then

207. score [ 1 ] [ "value" ] = score [ 1 ] [ "value" ] + 1

208. Shapes . SetText ( score [ 1 ] [ "obj" ] , score [ 1 ] [ "value" ] )

209. isGoal = "True"

210. If score [ 1 ] [ "value" ] = 7 Then

211. inGame = "False"

212. winner = 1

213. EndIf

214. Else

215. puck [ "cx" ] = field [ "x2" ] - puck [ "r" ] - ( x - ( field [ "x2" ] - puck [ "r" ] ) )

216. puck [ "vx" ] = - puck [ "vx" ]

217. Sound . PlayClick ( )

218. EndIf

219. Else

220. puck [ "cx" ] = x

221. EndIf

222. If isGoal Then

223. If y < goal [ "y" ] + puck [ "r" ] Then

224. y = goal [ "y" ] + puck [ "r" ]

225. ElseIf goal [ "y2" ] - puck [ "r" ] < y Then

226. y = goal [ "y2" ] - puck [ "r" ]

227. EndIf

228. Shapes . Move ( puck [ "obj" ] , x - puck [ "r" ] , y - puck [ "r" ] )

229. Sound . PlayChimeAndWait ( )

230. puck [ "cx" ] = gw / 2

231. puck [ "cy" ] = ( field [ "y" ] + field [ "y2" ] ) / 2

232. AdjustV0 ( )

233. Else

234. y = puck [ "cy" ] + dt * puck [ "vy" ]

235. If y < field [ "y" ] + puck [ "r" ] Then

236. puck [ "cy" ] = field [ "y" ] + puck [ "r" ] + ( field [ "y" ] + puck [ "r" ] - y )

237. puck [ "vy" ] = - puck [ "vy" ]

238. Sound . PlayClick ( )

239. ElseIf field [ "y2" ] - puck [ "r" ] < y Then

240. puck [ "cy" ] = field [ "y2" ] - puck [ "r" ] - ( y - ( field [ "y2" ] - puck [ "r" ] ) )

241. puck [ "vy" ] = - puck [ "vy" ]

242. Sound . PlayClick ( )

243. Else

244. puck [ "cy" ] = y

245. EndIf

246. CollisionCheck ( )

247. Shapes . Move ( puck [ "obj" ] , puck [ "cx" ] - puck [ "r" ] , puck [ "cy" ] - puck [ "r" ] )

248. EndIf

249. EndSub


Drawing Rounded Rectangle


This subroutine draws a rounded rectangle - a rectangle with rounded corners.



250. Sub DrawRoundRectangle

251. Stack . PushValue ( "local" , param )

252. Stack . PushValue ( "local" , local )

253. local = param

254. param = ""

255. param [ "r" ] = local [ "border-radius" ]

256. If ( local [ "width" ] / 2 < param [ "r" ] ) Or ( local [ "height" ] / 2 < param [ "r" ] ) Then

257. param [ "r" ] = Math . Min ( local [ "width" ] / 2 , local [ "height" ] / 2 )

258. EndIf

259. param [ "da" ] = 5

260. param [ "x" ] = local [ "x" ] + param [ "r" ]

261. param [ "y" ] = local [ "y" ] + param [ "r" ]

262. param [ "a1" ] = 180

263. param [ "a2" ] = 270

264. DrawArc ( )

265. GraphicsWindow . DrawLine ( local [ "x" ] + param [ "r" ] , local [ "y" ] , local [ "x" ] + local [ "width" ] - param [ "r" ] , local [ "y" ] )

266. param [ "x" ] = local [ "x" ] + local [ "width" ] - param [ "r" ]

267. param [ "y" ] = local [ "y" ] + param [ "r" ]

268. param [ "a1" ] = 270

269. param [ "a2" ] = 360

270. DrawArc ( )

271. GraphicsWindow . DrawLine ( local [ "x" ] + local [ "width" ] , local [ "y" ] + param [ "r" ] , local [ "x" ] + local [ "width" ] , local [ "y" ] + local [ "height" ] - param [ "r" ] )

272. param [ "x" ] = local [ "x" ] + local [ "width" ] - param [ "r" ]

273. param [ "y" ] = local [ "y" ] + local [ "height" ] - param [ "r" ]

274. param [ "a1" ] = 0

275. param [ "a2" ] = 90

276. DrawArc ( )

277. GraphicsWindow . DrawLine ( local [ "x" ] + param [ "r" ] , local [ "y" ] + local [ "height" ] , local [ "x" ] + local [ "width" ] - param [ "r" ] , local [ "y" ] + local [ "height" ] )

278. param [ "x" ] = local [ "x" ] + param [ "r" ]

279. param [ "y" ] = local [ "y" ] + local [ "height" ] - param [ "r" ]

280. param [ "a1" ] = 90

281. param [ "a2" ] = 180

282. DrawArc ( )

283. GraphicsWindow . DrawLine ( local [ "x" ] , local [ "y" ] + param [ "r" ] , local [ "x" ] , local [ "y" ] + local [ "height" ] - param [ "r" ] )

284. local = Stack . PopValue ( "local" )

285. param = Stack . PopValue ( "local" )

286. EndSub


Filling Rounded Rectangle


This subroutine fills a rounded rectangle with a color.



287. Sub FillRoundRectangle

288. Stack . PushValue ( "local" , param )

289. If ( param [ "width" ] / 2 < param [ "border-radius" ] ) Or ( param [ "height" ] / 2 < param [ "border-radius" ] ) Then

290. param [ "border-radius" ] = Math . Min ( param [ "width" ] / 2 , param [ "height" ] / 2 )

291. EndIf

292. GraphicsWindow . FillEllipse ( param [ "x" ] , param [ "y" ] , param [ "border-radius" ] * 2 , param [ "border-radius" ] * 2 )

293. GraphicsWindow . FillRectangle ( param [ "x" ] + param [ "border-radius" ] , param [ "y" ] , param [ "width" ] - param [ "border-radius" ] * 2 , param [ "height" ] )

294. GraphicsWindow . FillEllipse ( param [ "x" ] + param [ "width" ] - param [ "border-radius" ] * 2 , param [ "y" ] , param [ "border-radius" ] * 2 , param [ "border-radius" ] * 2 )

295. GraphicsWindow . FillRectangle ( param [ "x" ] , param [ "y" ] + param [ "border-radius" ] , param [ "width" ] , param [ "height" ] - param [ "border-radius" ] * 2 )

296. GraphicsWindow . FillEllipse ( param [ "x" ] , param [ "y" ] + param [ "height" ] - param [ "border-radius" ] * 2 , param [ "border-radius" ] * 2 , param [ "border-radius" ] * 2 )

297. GraphicsWindow . FillEllipse ( param [ "x" ] + param [ "width" ] - param [ "border-radius" ] * 2 , param [ "y" ] + param [ "height" ] - param [ "border-radius" ] * 2 , param [ "border-radius" ] * 2 , param [ "border-radius" ] * 2 )

298. param = Stack . PopValue ( "local" )

299. EndSub


Drawing Arc


This subroutine draws an arc.



300. Sub DrawArc

301. Stack . PushValue ( "local" , param )

302. Stack . PushValue ( "local" , local )

303. Stack . PushValue ( "local" , a )

304. local = param

305. param = ""

306. local [ "pw" ] = GraphicsWindow . PenWidth

307. local [ "pc" ] = GraphicsWindow . PenColor

308. local [ "bc" ] = GraphicsWindow . BrushColor

309. GraphicsWindow . BrushColor = local [ "pc" ]

310. local [ "r1" ] = local [ "r" ] - local [ "pw" ] / 2

311. local [ "r2" ] = local [ "r" ] + local [ "pw" ] / 2

312. For a = local [ "a1" ] To local [ "a2" ] Step local [ "da" ]

313. local [ "rad" ] = Math . GetRadians ( a )

314. param [ "x1" ] = local [ "x" ] + local [ "r1" ] * Math . Cos ( local [ "rad" ] )

315. param [ "y1" ] = local [ "y" ] + local [ "r1" ] * Math . Sin ( local [ "rad" ] )

316. param [ "x2" ] = local [ "x" ] + local [ "r2" ] * Math . Cos ( local [ "rad" ] )

317. param [ "y2" ] = local [ "y" ] + local [ "r2" ] * Math . Sin ( local [ "rad" ] )

318. If local [ "a1" ] < a Then

319. FillQuadrangle ( )

320. EndIf

321. param [ "x4" ] = param [ "x1" ]

322. param [ "y4" ] = param [ "y1" ]

323. param [ "x3" ] = param [ "x2" ]

324. param [ "y3" ] = param [ "y2" ]

325. EndFor

326. GraphicsWindow . BrushColor = local [ "bc" ]

327. a = Stack . PopValue ( "local" )

328. local = Stack . PopValue ( "local" )

329. param = Stack . PopValue ( "local" )

330. EndSub


Filling Quadrangle


This subroutines fills a quadrangle with a color.



331. Sub FillQuadrangle

332. GraphicsWindow . FillTriangle ( param [ "x1" ] , param [ "y1" ] , param [ "x2" ] , param [ "y2" ] , param [ "x3" ] , param [ "y3" ] )

333. GraphicsWindow . FillTriangle ( param [ "x3" ] , param [ "y3" ] , param [ "x4" ] , param [ "y4" ] , param [ "x1" ] , param [ "y1" ] )

334. EndSub


As this game, creating game with physical phenomena needs to use knowledge of physics.  This air hockey game doesn't affected by friction or gravity, but some games need to simulate them.


I'd like to end this game programming series for now.  Most of the games introduced here are born within the Small Basic MSDN Forum.  Especially, we can get a game challenge every month in Challenge of the Month .  I recommend you to challenge them.


Have a fun game programming!

Published Feb 12, 2019
Version 1.0
No CommentsBe the first to comment