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.5b2.' 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 = 59817.gh = 42818.GraphicsWindow.Width = gw19.GraphicsWindow.Height = gh20.GraphicsWindow.BackgroundColor = "DimGray"21.Field_Init()22.continue = "True"23.While continue24. Game_Init()25. Game_Start()26. Game_End()27.EndWhileField Initialization
This subroutine draws an air hockey field. And adds a puck and mallets for both players as Shapes.
28.Sub Field_Init29. fh = 30 ' font height30. GraphicsWindow.FontName = "Trebuchet MS"31. GraphicsWindow.BrushColor = "White"32. GraphicsWindow.FontSize = fh33. 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"] = 58038. field["height"] = 36039. field["x"] = (gw - field["width"]) / 240. field["y"] = (gh - field["height"] + fh) / 241. field["x2"] = field["x"] + field["width"]42. field["y2"] = field["y"] + field["height"]43. param["x"] = field["x"] - 1044. param["y"] = (field["y"] + field["y2"]) / 2 - 7045. param["width"] = 2046. param["height"] = 14047. param["border-radius"] = 1048. goal["y"] = param["y"]49. goal["y2"] = param["y"] + param["height"]50. GraphicsWindow.BrushColor = "Black"51. FillRoundRectangle()52. param["x"] = field["x2"] - 1053. FillRoundRectangle()54. GraphicsWindow.BrushColor = "Blue"55. GraphicsWindow.FillRectangle(field["x"], field["y"], field["width"], field["height"])56. GraphicsWindow.PenWidth = 557. GraphicsWindow.PenColor = "LightGray"58. param["x"] = field["x"] + 2059. param["y"] = field["y"] + 2060. param["width"] = field["width"] - 4061. param["height"] = field["height"] - 4062. param["border-radius"] = 10063. DrawRoundRectangle()64. x = param["x"] + param["width"] / 265. GraphicsWindow.DrawLine(x, param["y"], x, param["y"] + param["height"])66. GraphicsWindow.BrushColor = "Black"67. GraphicsWindow.PenWidth = 068. For y = field["y"] + 20 To field["y"] + field["height"] - 20 Step 2069. For x = field["x"] + 20 To field["x"] + field["width"] - 20 Step 2070. GraphicsWindow.FillEllipse(x - 1, y - 1, 2, 2)71. EndFor72. EndFor 73. GraphicsWindow.BrushColor = "Yellow"74. puck["size"] = 3475. puck["r"] = puck["size"] / 276. puck["obj"] = Shapes.AddEllipse(puck["size"], puck["size"])77. GraphicsWindow.BrushColor = "White"78. mallet[1]["size"] = 3479. mallet[1]["r"] = mallet[1]["size"] / 280. mallet[1]["obj"] = Shapes.AddEllipse(mallet[1]["size"], mallet[1]["size"])81. mallet[2]["size"] = 3482. mallet[2]["r"] = mallet[2]["size"] / 283. 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.EndSubGame Initialization
This subroutine initializes the position and the velocity of the puck and the positions of the mallets.
88.Sub Game_Init89. x = field["x"]90. y = field["y"] + field["height"] / 291. mallet[1]["cx"] = x + 20 92. mallet[1]["cy"] = y93. Shapes.Move(mallet[1]["obj"], mallet[1]["cx"] - mallet[1]["r"], mallet[1]["cy"] - mallet[1]["r"])94. mallet[2]["cx"] = x + field["width"] - 2095. mallet[2]["cy"] = y96. 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"] = y99. Shapes.Move(puck["obj"], puck["cx"] - puck["r"], puck["cy"] - puck["r"])100. v0 = 400101. puck["vx"] = 100102. puck["vy"] = 80103. AdjustV0()104. score[1]["value"] = 0105. Shapes.SetText(score[1]["obj"], score[1]["value"])106. score[2]["value"] = 0107. Shapes.SetText(score[2]["obj"], score[2]["value"])108. deltaY = puck["size"]109. GraphicsWindow.KeyDown = OnKeyDown110.EndSubPlaying 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_Start112. inGame = "True"113. dt = 1 / 24 ' [second]114. While inGame115. start = Clock.ElapsedMilliseconds116. UpdatePuck()117. delay = dt * 1000 - (Clock.ElapsedMilliseconds - start)118. If 0 < delay Then119. Program.Delay(delay)120. EndIf121. EndWhile122.EndSubGame End
This subroutine shows the winner.
123.Sub Game_End124. Shapes.SetOpacity(screen, 40)125. GraphicsWindow.FontSize = 40126. GraphicsWindow.BrushColor = "White"127. result = Shapes.AddText("PLAYER " + winner + " WON")128. x = (gw - 283) / 2129. y = (gh - 40) / 2130. Shapes.Move(result, x, y)131. Sound.PlayBellRingAndWait()132. Program.Delay(5000)133. Shapes.SetOpacity(screen, 0)134. Shapes.Remove(result)135.EndSubAdjusting 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 AdjustV0137. v = Math.SquareRoot(Math.Power(puck["vx"], 2) + Math.Power(puck["vy"], 2))138. puck["vx"] = puck["vx"] * v0 / v139. puck["vy"] = puck["vy"] * v0 / v140.EndSubCollision 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 CollisionCheck142. For i = 1 To 2143. 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"] Then147. Sound.PlayClick()148. relativeVx = puck["vx"]149. relativeVy = puck["vy"]150. nx = dx / distance151. ny = dy / distance152. l = nx * relativeVx + ny * relativeVy153. relativeVx = relativeVx - (2 * l * nx)154. relativeVy = relativeVy - (2 * l * ny)155. puck["vx"] = relativeVx156. puck["vy"] = relativeVy157. puck["cx"] = puck["cx"] - nx * (puck["size"] - distance)158. puck["cy"] = puck["cy"] - ny * (puck["size"] - distance)159. EndIf160. EndFor161.EndSubKey Input Event Handler
This subroutine moves the mallets when the keys are input.
162.Sub OnKeyDown163. key = GraphicsWindow.LastKey164. If key = "W" Then ' player 1 up165. If goal["y"] <= mallet[1]["cy"] - deltaY Then166. mallet[1]["cy"] = mallet[1]["cy"] - deltaY167. Shapes.Move(mallet[1]["obj"], mallet[1]["cx"] - mallet[1]["r"], mallet[1]["cy"] - mallet[1]["r"])168. EndIf169. ElseIf key = "S" Then ' player 1 down170. If mallet[1]["cy"] + deltaY <= goal["y2"] Then171. mallet[1]["cy"] = mallet[1]["cy"] + deltaY172. Shapes.Move(mallet[1]["obj"], mallet[1]["cx"] - mallet[1]["r"], mallet[1]["cy"] - mallet[1]["r"])173. EndIf174. ElseIf key = "O" Then ' player 2 up175. If goal["y"] <= mallet[2]["cy"] - deltaY Then176. mallet[2]["cy"] = mallet[2]["cy"] - deltaY177. Shapes.Move(mallet[2]["obj"], mallet[2]["cx"] - mallet[2]["r"], mallet[2]["cy"] - mallet[2]["r"])178. EndIf179. ElseIf key = "L" Then ' player 2 down180. If mallet[2]["cy"] + deltaY <= goal["y2"] Then181. mallet[2]["cy"] = mallet[2]["cy"] + deltaY182. Shapes.Move(mallet[2]["obj"], mallet[2]["cx"] - mallet[2]["r"], mallet[2]["cy"] - mallet[2]["r"])183. EndIf184. EndIf185.EndSubUpdate 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 UpdatePuck187. isGoal = "False"188. x = puck["cx"] + dt * puck["vx"]189. If x < field["x"] + puck["r"] Then190. y = puck["cy"] + dt * (field["x"] - puck["cx"]) * puck["vy"] / puck["vx"]191. If (goal["y"] < y) And (y < goal["y2"]) Then192. score[2]["value"] = score[2]["value"] + 1193. Shapes.SetText(score[2]["obj"], score[2]["value"])194. isGoal = "True"195. If score[2]["value"] = 7 Then196. inGame = "False"197. winner = 2198. EndIf199. Else200. puck["cx"] = field["x"] + puck["r"] + (field["x"] + puck["r"] - x)201. puck["vx"] = -puck["vx"]202. Sound.PlayClick()203. EndIf204. ElseIf field["x2"] - puck["r"] < x Then205. y = puck["cy"] + dt * (field["x2"] - puck["cx"]) * puck["vy"] / puck["vx"]206. If (goal["y"] < y) And (y < goal["y2"]) Then207. score[1]["value"] = score[1]["value"] + 1208. Shapes.SetText(score[1]["obj"], score[1]["value"])209. isGoal = "True"210. If score[1]["value"] = 7 Then211. inGame = "False"212. winner = 1213. EndIf214. Else215. puck["cx"] = field["x2"] - puck["r"] - (x - (field["x2"] - puck["r"]))216. puck["vx"] = -puck["vx"]217. Sound.PlayClick()218. EndIf219. Else220. puck["cx"] = x221. EndIf222. If isGoal Then223. If y < goal ["y"] + puck["r"] Then224. y = goal["y"] + puck["r"]225. ElseIf goal["y2"] - puck["r"] < y Then226. y = goal["y2"] - puck["r"]227. EndIf228. Shapes.Move(puck["obj"], x - puck["r"], y - puck["r"])229. Sound.PlayChimeAndWait()230. puck["cx"] = gw / 2231. puck["cy"] = (field["y"] + field["y2"]) / 2232. AdjustV0()233. Else234. y = puck["cy"] + dt * puck["vy"]235. If y < field["y"] + puck["r"] Then236. 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 Then240. puck["cy"] = field["y2"] - puck["r"] - (y - (field["y2"] - puck["r"]))241. puck["vy"] = -puck["vy"]242. Sound.PlayClick()243. Else244. puck["cy"] = y245. EndIf246. CollisionCheck()247. Shapes.Move(puck["obj"], puck["cx"] - puck["r"], puck["cy"] - puck["r"])248. EndIf249.EndSubDrawing Rounded Rectangle
This subroutine draws a rounded rectangle - a rectangle with rounded corners.
250.Sub DrawRoundRectangle251. Stack.PushValue("local", param)252. Stack.PushValue("local", local)253. local = param254. param = ""255. param["r"] = local["border-radius"]256. If (local["width"] / 2 < param["r"]) Or (local["height"] / 2 < param["r"]) Then257. param["r"] = Math.Min(local["width"] / 2, local["height"] / 2)258. EndIf259. param["da"] = 5260. param["x"] = local["x"] + param["r"]261. param["y"] = local["y"] + param["r"]262. param["a1"] = 180263. param["a2"] = 270264. 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"] = 270269. param["a2"] = 360270. 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"] = 0275. param["a2"] = 90276. 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"] = 90281. param["a2"] = 180282. 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.EndSubFilling Rounded Rectangle
This subroutine fills a rounded rectangle with a color.
287.Sub FillRoundRectangle288. Stack.PushValue("local", param)289. If (param["width"] / 2 < param["border-radius"]) Or (param["height"] / 2 < param["border-radius"]) Then290. param["border-radius"] = Math.Min(param["width"] / 2, param["height"] / 2)291. EndIf292. 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.EndSubDrawing Arc
This subroutine draws an arc.
300.Sub DrawArc301. Stack.PushValue("local", param)302. Stack.PushValue("local", local)303. Stack.PushValue("local", a)304. local = param305. param = ""306. local["pw"] = GraphicsWindow.PenWidth307. local["pc"] = GraphicsWindow.PenColor308. local["bc"] = GraphicsWindow.BrushColor309. GraphicsWindow.BrushColor = local["pc"]310. local["r1"] = local["r"] - local["pw"] / 2311. local["r2"] = local["r"] + local["pw"] / 2312. 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 Then319. FillQuadrangle()320. EndIf321. param["x4"] = param["x1"]322. param["y4"] = param["y1"]323. param["x3"] = param["x2"]324. param["y3"] = param["y2"]325. EndFor326. GraphicsWindow.BrushColor = local["bc"]327. a = Stack.PopValue("local")328. local = Stack.PopValue("local")329. param = Stack.PopValue("local")330.EndSubFilling Quadrangle
This subroutines fills a quadrangle with a color.
331.Sub FillQuadrangle332. 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.EndSubAs 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!