« プログラミング講座(159) 射撃ゲーム | トップページ | プログラミング講座(161) ビッグチャレンジ »

2014/08/18

プログラミング講座(160) エアホッケー

2013年7月と2014年7月に今月のチャレンジで出題されたエアホッケー。丸1年で完成しました。プレイヤー1(左側)は[W]キーと[S]キー、プレイヤー2(右側)は[O][L]キーを使ってマレットを動かします。7点先取したほうが勝ちです。プログラムを KLB414-4 で発行しました。

図146 エアホッケー
【図146 エアホッケー】

メイン

メインループではゲームを繰り返します。変数 continue は常に "True" です。

' Air Hockey 0.5b
' Copyright (c) 2013-2014 Nonki Takahashi. MIT License.
'
' History:
' 0.5b 2014-07-09 Determined a winner is the first to win 7 points. (KLB414-4)
' 0.4b 2014-07-09 Supported collision between puck and mallet. (KLB414-3)
' 0.3a 2014-07-03 Added mallet control. (KLB414-2)
' 0.2a 2013-07-30 Changed field design. (KLB414-1)
' 0.11a 2013-07-29 Modified for Silverlight. (KLB414-0)
' 0.1a 2013-07-29 Created as alpha version. (KLB414)
'
' Reference:
' LitDev, Small Basic: Dynamic Graphics, TechNet Wiki, 2013-2014.
'
GraphicsWindow.Title = "Air Hockey 0.5b - W,S for P1; O,L for P2"
gw = 598
gh = 428
GraphicsWindow.Width = gw
GraphicsWindow.Height = gh
GraphicsWindow.BackgroundColor = "DimGray"
Field_Init()
continue = "True"
While continue
 Game_Init()
 Game_Start()
 Game_End()
EndWhile

フィールドの初期化

エアホッケーのフィールドを描画します。またパックと両プレイヤーのマレットを Shapes (図形)として作成します。

Sub Field_Init
 fh = 30 ' font height
 GraphicsWindow.FontName = "Trebuchet MS"
 GraphicsWindow.BrushColor = "White"
 GraphicsWindow.FontSize = fh
 score[1]["obj"] = Shapes.AddText(0)
 Shapes.Move(score[1]["obj"], gw / 2 - 100, 10)
 score[2]["obj"] = Shapes.AddText(0)
 Shapes.Move(score[2]["obj"], gw / 2 + 100, 10)
 field["width"] = 580
 field["height"] = 360
 field["x"] = (gw - field["width"]) / 2
 field["y"] = (gh - field["height"] + fh) / 2
 field["x2"] = field["x"] + field["width"]
 field["y2"] = field["y"] + field["height"]
 param["x"] = field["x"] - 10
 param["y"] = (field["y"] + field["y2"]) / 2 - 70
 param["width"] = 20
 param["height"] = 140
 param["border-radius"] = 10
 goal["y"] = param["y"]
 goal["y2"] = param["y"] + param["height"]
 GraphicsWindow.BrushColor = "Black"
 FillRoundRectangle()
 param["x"] = field["x2"] - 10
 FillRoundRectangle()
 GraphicsWindow.BrushColor = "Blue"
 GraphicsWindow.FillRectangle(field["x"], field["y"], field["width"], field["height"])
 GraphicsWindow.PenWidth = 5
 GraphicsWindow.PenColor = "LightGray"
 param["x"] = field["x"] + 20
 param["y"] = field["y"] + 20
 param["width"] = field["width"] - 40
 param["height"] = field["height"] - 40
 param["border-radius"] = 100
 DrawRoundRectangle()
 x = param["x"] + param["width"] / 2
 GraphicsWindow.DrawLine(x, param["y"], x, param["y"] + param["height"])
 GraphicsWindow.BrushColor = "Black"
 GraphicsWindow.PenWidth = 0
 For y = field["y"] + 20 To field["y"] + field["height"] - 20 Step 20
  For x = field["x"] + 20 To field["x"] + field["width"] - 20 Step 20
   GraphicsWindow.FillEllipse(x - 1, y - 1, 2, 2)
  EndFor
 EndFor
 GraphicsWindow.BrushColor = "Yellow"
 puck["size"] = 34
 puck["r"] = puck["size"] / 2
 puck["obj"] = Shapes.AddEllipse(puck["size"], puck["size"])
 GraphicsWindow.BrushColor = "White"
 mallet[1]["size"] = 34
 mallet[1]["r"] = mallet[1]["size"] / 2
 mallet[1]["obj"] = Shapes.AddEllipse(mallet[1]["size"], mallet[1]["size"])
 mallet[2]["size"] = 34
 mallet[2]["r"] = mallet[2]["size"] / 2
 mallet[2]["obj"] = Shapes.AddEllipse(mallet[2]["size"], mallet[2]["size"])
 GraphicsWindow.BrushColor = "DimGray"
 screen = Shapes.AddRectangle(gw, gh)
 Shapes.SetOpacity(screen, 0)
EndSub

ゲームの初期化

パックの位置と速度、マレットの位置を初期化します。

Sub Game_Init
 x = field["x"]
 y = field["y"] + field["height"] / 2
 mallet[1]["cx"] = x + 20
 mallet[1]["cy"] = y
 Shapes.Move(mallet[1]["obj"], mallet[1]["cx"] - mallet[1]["r"], mallet[1]["cy"] - mallet[1]["r"])
 mallet[2]["cx"] = x + field["width"] - 20
 mallet[2]["cy"] = y
 Shapes.Move(mallet[2]["obj"], mallet[2]["cx"] - mallet[2]["r"], mallet[2]["cy"] - mallet[2]["r"])
 puck["cx"] = x + (field["width"] / 2)
 puck["cy"] = y
 Shapes.Move(puck["obj"], puck["cx"] - puck["r"], puck["cy"] - puck["r"])
 v0 = 400
 puck["vx"] = 100
 puck["vy"] = 80
 AdjustV0()
 score[1]["value"] = 0
 Shapes.SetText(score[1]["obj"], score[1]["value"])
 score[2]["value"] = 0
 Shapes.SetText(score[2]["obj"], score[2]["value"])
 deltaY = puck["size"]
 GraphicsWindow.KeyDown = OnKeyDown
EndSub

ゲームのプレイ

一方のプレイヤーが7点先取するまでループし、ゲームを続けます。ループの中で1秒間に24回パックの位置と速度を更新します。

Sub Game_Start
 inGame = "True"
 dt = 1 / 24 ' [second]
 While inGame
  start = Clock.ElapsedMilliseconds
  UpdatePuck()
  delay = dt * 1000 - (Clock.ElapsedMilliseconds - start)
  If 0 < delay Then
   Program.Delay(delay)
  EndIf
 EndWhile
EndSub

ゲームの終了

勝利したプレイヤーを表示します。

Sub Game_End
 Shapes.SetOpacity(screen, 40)
 GraphicsWindow.FontSize = 40
 GraphicsWindow.BrushColor = "White"
 result = Shapes.AddText("PLAYER " + winner + " WON")
 x = (gw - 283) / 2
 y = (gh - 40) / 2
 Shapes.Move(result, x, y)
 Sound.PlayBellRingAndWait()
 Program.Delay(5000)
 Shapes.SetOpacity(screen, 0)
 Shapes.Remove(result)
EndSub

パックの速度調整

パックの速度 puck["vx"], puck["vy"] のスカラー値が v0 になるよう調整します。

Sub AdjustV0
 v = Math.SquareRoot(Math.Power(puck["vx"], 2) + Math.Power(puck["vy"], 2))
 puck["vx"] = puck["vx"] * v0 / v
 puck["vy"] = puck["vy"] * v0 / v
EndSub

衝突の検出

パックとマレットの衝突を検出し、衝突した場合はパックの速度を更新します。このサブルーチンは LitDev 氏による TechNet Wiki の記事 Dynamic Graphics の同名のサブルーチンを参考にしています。オリジナルはボール同士の衝突ですが、ここではマレットはプレイヤーによって保持されるため、パックだけが跳ね返るように変更しました。

Sub CollisionCheck
 For i = 1 To 2
  dx = mallet[i]["cx"] - puck["cx"]
  dy = mallet[i]["cy"] - puck["cy"]
  distance = Math.SquareRoot(dx * dx + dy * dy)
  If distance < puck["size"] Then
   Sound.PlayClick()
   relativeVx = puck["vx"]
   relativeVy = puck["vy"]
   nx = dx / distance
   ny = dy / distance
   l = nx * relativeVx + ny * relativeVy
   relativeVx = relativeVx - (2 * l * nx)
   relativeVy = relativeVy - (2 * l * ny)
   puck["vx"] = relativeVx
   puck["vy"] = relativeVy
   puck["cx"] = puck["cx"] - nx * (puck["size"] - distance)
   puck["cy"] = puck["cy"] - ny * (puck["size"] - distance)
  EndIf
 EndFor
EndSub

キー入力イベントハンドラ

キー入力時、マレットを動かします。

Sub OnKeyDown
 key = GraphicsWindow.LastKey
 If key = "W" Then ' player 1 up
  If goal["y"] <= mallet[1]["cy"] - deltaY Then
   mallet[1]["cy"] = mallet[1]["cy"] - deltaY
   Shapes.Move(mallet[1]["obj"], mallet[1]["cx"] - mallet[1]["r"], mallet[1]["cy"] - mallet[1]["r"])
  EndIf
 ElseIf key = "S" Then ' player 1 down
  If mallet[1]["cy"] + deltaY <= goal["y2"] Then
   mallet[1]["cy"] = mallet[1]["cy"] + deltaY
   Shapes.Move(mallet[1]["obj"], mallet[1]["cx"] - mallet[1]["r"], mallet[1]["cy"] - mallet[1]["r"])
  EndIf
 ElseIf key = "O" Then ' player 2 up
  If goal["y"] <= mallet[2]["cy"] - deltaY Then
   mallet[2]["cy"] = mallet[2]["cy"] - deltaY
   Shapes.Move(mallet[2]["obj"], mallet[2]["cx"] - mallet[2]["r"], mallet[2]["cy"] - mallet[2]["r"])
  EndIf
 ElseIf key = "L" Then ' player 2 down
  If mallet[2]["cy"] + deltaY <= goal["y2"] Then
   mallet[2]["cy"] = mallet[2]["cy"] + deltaY
   Shapes.Move(mallet[2]["obj"], mallet[2]["cx"] - mallet[2]["r"], mallet[2]["cy"] - mallet[2]["r"])
  EndIf
 EndIf
EndSub

パックの速度と位置の更新

パックは慣性の法則にしたがって同じ速度で移動するので、それをシミュレートします。ゴールの判定、フィールドの枠との衝突の検出も行います。最後に CollisionCheck() を呼び出してマレットとの衝突の検出も行います。ゴールが決まった場合はパックの位置を中央に戻します。一方が7点先取した場合は、inGame フラグに "False" を設定し、Game_Start() 内のループを抜けるようにします。

Sub UpdatePuck
 isGoal = "False"
 x = puck["cx"] + dt * puck["vx"]
 If x < field["x"] + puck["r"] Then
  y = puck["cy"] + dt * (field["x"] - puck["cx"]) * puck["vy"] / puck["vx"]
  If (goal["y"] < y) And (y < goal["y2"]) Then
   score[2]["value"] = score[2]["value"] + 1
   Shapes.SetText(score[2]["obj"], score[2]["value"])
   isGoal = "True"
   If score[2]["value"] = 7 Then
    inGame = "False"
    winner = 2
   EndIf
  Else
   puck["cx"] = field["x"] + puck["r"] + (field["x"] + puck["r"] - x)
   puck["vx"] = -puck["vx"]
   Sound.PlayClick()
  EndIf
 ElseIf field["x2"] - puck["r"] < x Then
  y = puck["cy"] + dt * (field["x2"] - puck["cx"]) * puck["vy"] / puck["vx"]
  If (goal["y"] < y) And (y < goal["y2"]) Then
   score[1]["value"] = score[1]["value"] + 1
   Shapes.SetText(score[1]["obj"], score[1]["value"])
   isGoal = "True"
   If score[1]["value"] = 7 Then
    inGame = "False"
    winner = 1
   EndIf
  Else
   puck["cx"] = field["x2"] - puck["r"] - (x - (field["x2"] - puck["r"]))
   puck["vx"] = -puck["vx"]
   Sound.PlayClick()
  EndIf
 Else
  puck["cx"] = x
 EndIf
 If isGoal Then
  If y < goal ["y"] + puck["r"] Then
   y = goal["y"] + puck["r"]
  ElseIf goal["y2"] - puck["r"] < y Then
   y = goal["y2"] - puck["r"]
  EndIf
  Shapes.Move(puck["obj"], x - puck["r"], y - puck["r"])
  Sound.PlayChimeAndWait()
  puck["cx"] = gw / 2
  puck["cy"] = (field["y"] + field["y2"]) / 2
  AdjustV0()
 Else
  y = puck["cy"] + dt * puck["vy"]
  If y < field["y"] + puck["r"] Then
   puck["cy"] = field["y"] + puck["r"] + (field["y"] + puck["r"] - y)
   puck["vy"] = -puck["vy"]
   Sound.PlayClick()
  ElseIf field["y2"] - puck["r"] < y Then
   puck["cy"] = field["y2"] - puck["r"] - (y - (field["y2"] - puck["r"]))
   puck["vy"] = -puck["vy"]
   Sound.PlayClick()
  Else
   puck["cy"] = y
  EndIf
  CollisionCheck()
  Shapes.Move(puck["obj"], puck["cx"] - puck["r"], puck["cy"] - puck["r"])
 EndIf
EndSub

ラウンド長方形の描画

角が丸い長方形の枠を描画します。

Sub DrawRoundRectangle
 Stack.PushValue("local", param)
 Stack.PushValue("local", local)
 local = param
 param = ""
 param["r"] = local["border-radius"]
 If (local["width"] / 2 < param["r"]) Or (local["height"] / 2 < param["r"]) Then
  param["r"] = Math.Min(local["width"] / 2, local["height"] / 2)
 EndIf
 param["da"] = 5
 param["x"] = local["x"] + param["r"]
 param["y"] = local["y"] + param["r"]
 param["a1"] = 180
 param["a2"] = 270
 DrawArc()
 GraphicsWindow.DrawLine(local["x"] + param["r"], local["y"], local["x"] + local["width"] - param["r"], local["y"])
 param["x"] = local["x"] + local["width"] - param["r"]
 param["y"] = local["y"] + param["r"]
 param["a1"] = 270
 param["a2"] = 360
 DrawArc()
 GraphicsWindow.DrawLine(local["x"] + local["width"], local["y"] + param["r"], local["x"] + local["width"], local["y"] + local["height"] - param["r"])
 param["x"] = local["x"] + local["width"] - param["r"]
 param["y"] = local["y"] + local["height"] - param["r"]
 param["a1"] = 0
 param["a2"] = 90
 DrawArc()
 GraphicsWindow.DrawLine(local["x"] + param["r"], local["y"] + local["height"], local["x"] + local["width"] - param["r"], local["y"] + local["height"])
 param["x"] = local["x"] + param["r"]
 param["y"] = local["y"] + local["height"] - param["r"]
 param["a1"] = 90
 param["a2"] = 180
 DrawArc()
 GraphicsWindow.DrawLine(local["x"], local["y"] + param["r"], local["x"], local["y"] + local["height"] - param["r"])
 local = Stack.PopValue("local")
 param = Stack.PopValue("local")
EndSub

ラウンド長方形の塗りつぶし

角が丸い長方形の内部を塗りつぶします。

Sub FillRoundRectangle
 Stack.PushValue("local", param)
 If (param["width"] / 2 < param["border-radius"]) Or (param["height"] / 2 < param["border-radius"]) Then
  param["border-radius"] = Math.Min(param["width"] / 2, param["height"] / 2)
 EndIf
 GraphicsWindow.FillEllipse(param["x"], param["y"], param["border-radius"] * 2, param["border-radius"] * 2)
 GraphicsWindow.FillRectangle(param["x"] + param["border-radius"], param["y"], param["width"] - param["border-radius"] * 2, param["height"])
 GraphicsWindow.FillEllipse(param["x"] + param["width"] - param["border-radius"] * 2, param["y"], param["border-radius"] * 2, param["border-radius"] * 2)
 GraphicsWindow.FillRectangle(param["x"], param["y"] + param["border-radius"], param["width"], param["height"] - param["border-radius"] * 2)
 GraphicsWindow.FillEllipse(param["x"], param["y"] + param["height"] - param["border-radius"] * 2, param["border-radius"] * 2, param["border-radius"] * 2)
 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)
 param = Stack.PopValue("local")
EndSub

弧の描画

弧を描画します。

Sub DrawArc
 Stack.PushValue("local", param)
 Stack.PushValue("local", local)
 Stack.PushValue("local", a)
 local = param
 param = ""
 local["pw"] = GraphicsWindow.PenWidth
 local["pc"] = GraphicsWindow.PenColor
 local["bc"] = GraphicsWindow.BrushColor
 GraphicsWindow.BrushColor = local["pc"]
 local["r1"] = local["r"] - local["pw"] / 2
 local["r2"] = local["r"] + local["pw"] / 2
 For a = local["a1"] To local["a2"] Step local["da"]
  local["rad"] = Math.GetRadians(a)
  param["x1"] = local["x"] + local["r1"] * Math.Cos(local["rad"])
  param["y1"] = local["y"] + local["r1"] * Math.Sin(local["rad"])
  param["x2"] = local["x"] + local["r2"] * Math.Cos(local["rad"])
  param["y2"] = local["y"] + local["r2"] * Math.Sin(local["rad"])
  If local["a1"] < a Then
   FillQuadrangle()
  EndIf
  param["x4"] = param["x1"]
  param["y4"] = param["y1"]
  param["x3"] = param["x2"]
  param["y3"] = param["y2"]
 EndFor
 GraphicsWindow.BrushColor = local["bc"]
 a = Stack.PopValue("local")
 local = Stack.PopValue("local")
 param = Stack.PopValue("local")
EndSub

四角形の塗りつぶし

四角形の内部を塗りつぶします。

Sub FillQuadrangle
 GraphicsWindow.FillTriangle(param["x1"], param["y1"], param["x2"], param["y2"], param["x3"], param["y3"])
 GraphicsWindow.FillTriangle(param["x3"], param["y3"], param["x4"], param["y4"], param["x1"], param["y1"])
EndSub

このゲームのように物理現象を扱うゲームでは、物理の知識を活用します。このエアホッケーでは摩擦も重力も影響しませんが、ゲームによってはそれらの知識も必要になります。

今回でゲームプログラミングチュートリアルは一旦終了とします。紹介したゲームは Small Basic に関する MSDNフォーラムで誕生しました。特に今月のチャレンジには毎回ゲームの問題が出題されています。是非挑戦してみてください。

|

« プログラミング講座(159) 射撃ゲーム | トップページ | プログラミング講座(161) ビッグチャレンジ »

Small Basic」カテゴリの記事

ゲームプログラミング」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




トラックバック


この記事へのトラックバック一覧です: プログラミング講座(160) エアホッケー:

« プログラミング講座(159) 射撃ゲーム | トップページ | プログラミング講座(161) ビッグチャレンジ »