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!