« プログラミング講座(158) 垂直スクロールゲーム(解説その2) | トップページ | プログラミング講座(160) エアホッケー »

2014/08/03

プログラミング講座(159) 射撃ゲーム

2013年の8月のチャレンジ問題に(1)アヒルの射撃ゲームのオープニングを作れ、(2)アヒルの射撃ゲームを作れというものがありました。私はそのとき前半だけに挑戦しました。今回後半部分に着手し、プログラムID TLR995-2 として発行しました。

全体として425行あるのですが、図形エディタによって自動生成された部分が166行あるので、実際にコーディングしたのは259行です。

メイン

射撃部分を一応追加してありますが、アヒルが一羽しか出てきません。増やすとプログラムが複雑になって読みにくくなると考えたからです。

' DuckShoot 0.31b
' Copyright (c) 2013-2014 Nonki Takahashi. The MIT License.
'
' History:
' 0.31b 2014-08-01 Sorted subroutines. (TLR995-2)
' 0.3b 2014-07-26 Supported in remote. (TLR995-1)
' 0.2b 2014-07-13 Created a core of shooting. (TLR995-0)
' 0.1a 2013-08-03 Created as DuckShoot opening. (TLR995)
' 0.0 2013-08-03 14:38:49 Shapes generated by Shapes 1.5b.
'
GraphicsWindow.Title = "DuckShoot 0.31b"
SB_Workaround()
Opening()
GameInit()
GameLoop()
Ending()

エンディング

撃った数と当った数から最後の点数を計算して表示しています。

Sub Ending
 Shapes.Remove(stair)
 If hit[i] Then
  point = 1
 Else
  point = 0
 EndIf
 score = point * 110 - shoot * 10
 GraphicsWindow.BrushColor = "White"
 GraphicsWindow.FontSize = 40
 GraphicsWindow.DrawText(170, 180, "SCORE " + score)
 Program.Delay(500)
 GraphicsWindow.DrawText(170, 230, "SHOOT " + shoot)
 Program.Delay(500)
 GraphicsWindow.DrawText(170, 280, "HIT " + point)
 Program.Delay(2000)
 GraphicsWindow.FontSize = 50
 GraphicsWindow.DrawText(170, 80, "GAME OVER")
EndSub

初期化

アヒルは一羽しかいないのですが、将来のため、アヒルに関する変数を配列にしてあります。このゲームではプレイヤーはマウスで銃の操作することにしたので、マウスのクリックと移動のイベントを使います。

Sub GameInit
 ' Game start
 Shapes.ShowShape(duck[1])
 mouseDown = "False"
 GraphicsWindow.MouseDown = OnMouseDown
 GraphicsWindow.MouseMove = OnMouseMove
 Shapes.Animate(duck[1], gw, 150, 3000)
 Program.Delay(3000)
 i = 1
 hit[i] = "False"
 a[i] = 90
 x[i] = -dw
 yDuck = 150
 y[i] = yDuck
 Shapes.Move(duck[i], x[i], y[i])
 GraphicsWindow.PenWidth = 0
 GraphicsWindow.BrushColor = bgColor
 yRS = yStair - (yDuck + dh / 2)
 shoot = 0
EndSub

メインループ

前回はタイマーイベントの中で障害物を移動させましたが、今回はメインループの中でアヒルを4ドットずつ右に移動させています。

プレイヤーが銃を撃ったとき、それが当たったかどうかを判定するために、移動している Shapes のアヒルのイメージの裏にもう一枚のアヒルを描画しています。標的が円や長方形であればマウス座標と標的の座標から計算で当たったかどうかの判定ができます。今回のアヒルは複雑な図形なので、計算するのではなく、GraphicsWindow.GetPixel() で得られる色が背景の色かどうかという判定にしました。

ここでブラウザ上の問題が発生しました。GraphicsWindow.DrawImage() の後に GraphicsWindow.GetPixel() を呼び出すとプログラムがハングし(応答しなくなり)ます。IDE にインポートした場合は問題ありません。この問題を回避するために、DrawImage() を使うのではなく、図形エディタで作成したデータ shape を元に FillRectangle(), FillTriangle(), FillEllipse() を使うようにしました。これはアヒルのデータを図形エディタで作ったのでうまくいきました。

当たった場合、イメージを Shapes.Zoom() を使って倒します。このオペレーションは与える拡大レベルが 0.1 から 20 までの範囲なので注意が必要です。今回は 0.1 になるまで拡大レベルを少しずつ減らしています。

ここで今度はIDE にインポートしたプログラム上で問題が発生しました。アヒルのイメージを 0.7 より小さくするとプログラムが停止します。このアヒルのイメージは背景の部分が透明でした。透明の部分があるイメージを 0.7 より小さく縮小するとこの問題が発生します。ブラウザ上ではこの問題は起きません。この問題を回避するために、背景の色を不透明にしたイメージを使うことにしました。

Sub GameLoop
 While x[i] < gw
  Program.Delay(50)
  If mouseDown Then
   Sound.PlayClick()
   shoot = shoot + 1
   If silverlight Then
    shX = x[i]
    shY = y[i]
    iMin = 1
    iMax = 10
    Shapes_Draw()
   Else
    GraphicsWindow.DrawImage(img, x[i], y[i])
   EndIf
   color = GraphicsWindow.GetPixel(dx, dy)
   GraphicsWindow.PenWidth = 0
   GraphicsWindow.BrushColor = bgColor
   GraphicsWindow.FillRectangle(x[i], y[i], dw, dh)
   If color <> bgColor Then
    hit[i] = "True"
   EndIf
   mouseDown = "False"
  EndIf
  If hit[i] Then
   If 0 < a[i] Then
    a[i] = a[i] - 5
    cos = Math.Round(Math.Sin(Math.GetRadians(a[i])) * 100) / 100
    Shapes.Zoom(duck[i], 1, Math.Max(cos, 0.1))
    deltaY = yRS - yRS * cos
    y[i] = yDuck + deltaY
   EndIf
  EndIf
  x[i] = x[i] + 4
  Shapes.Move(duck[i], x[i], y[i])
 EndWhile
EndSub

マウスイベントハンドラ(クリック時)

フラグを立ててマウス座標を保存します。GameLoop() の中でこの座標を元に撃った弾がアヒルに当たるかどうかを判定します。

Sub OnMouseDown
 mouseDown = "True"
 dx = GraphicsWindow.MouseX
 dy = GraphicsWindow.MouseY
EndSub

マウスイベントハンドラ(移動時)

マウスの動きに合わせて照準を移動します。ただし、マウスポインタがウィンドウ内にあるときはマウスポインタ(矢印のマーク)を消し、ウィンドウ外ではマウスポインタを表示します。

Sub OnMouseMove
 mx = GraphicsWindow.MouseX
 my = GraphicsWindow.MouseY
 If 0 <= mx And mx < gw And 0 <= my And my < gh Then
  Mouse.HideCursor()
  Shapes.Move(sighter, mx - 40, my - 40)
 Else
  Mouse.ShowCursor()
 EndIf
EndSub

オープニング

ゲームタイトルとアヒル・照準のイメージを表示します。元々は図形エディタで作成した図形の組み合わせだったのですが、よりスムーズに移動させるためアヒルと照準のイメージは .png ファイルに変換しました。図形エディタで作成した図形を .png ファイルに変換する方法については、こちら(英語)をご覧ください。

ただし、アヒルに関しては2つの理由でイメージと図形の両方を使っています。一つはオープニングで瞬きさせるため、もう一つは前述したブラウザ上での問題を回避するためです。

Sub Opening
 bgColor = "#8B0000" ' DarkRed
 stColor = "#990000" ' for stair
 GraphicsWindow.BackgroundColor = bgColor
 gw = 598
 gh = 428
 GraphicsWindow.Width = gw
 GraphicsWindow.Height = gh
 GraphicsWindow.PenWidth = 0
 GraphicsWindow.BrushColor = bgColor
 GraphicsWindow.FillRectangle(0, 0, gw, gh)
 ' add duck image
 path = "http://gallery.technet.microsoft.com/site/view/file/119954/1/Duck2.png"
 img = ImageList.LoadImage(path)
 If silverlight Then
  dw = 246 + 1
  dh = 192 + 2
 Else
  dw = ImageList.GetWidthOfImage(img)
  dh = ImageList.GetHeightOfImage(img)
 EndIf
 duck[1] = Shapes.AddImage(img)
 Shapes.Move(duck[1], 194, 150)
 Shapes.HideShape(duck[1])
 ' add stair
 GraphicsWindow.BrushColor = stColor
 GraphicsWindow.PenWidth = 0
 stair = Shapes.AddRectangle(gw, gh - yStair)
 yStair = Math.Round(gh * 2 / 3)
 Shapes.Move(stair, 0, yStair)
 Shapes.HideShape(stair)
 ' initialize shapes
 GraphicsWindow.FontName = "Trebuchet MS"
 GraphicsWindow.FontSize = 50
 GraphicsWindow.BrushColor = "White"
 title = Shapes.AddText("DuckShoot")
 Shapes.Move(title, 170, 60)
 Shapes_Init()
 ' add shapes
 scale = 1
 angle = 0
 iMin = 1
 iMax = 10
 Shapes_Add()
 ' add sighter image
 path = "http://gallery.technet.microsoft.com/site/view/file/119955/1/Sighter.png"
 sighter = Shapes.AddImage(path)
 Shapes.Move(sighter, 250, 200)
 ' Blink start
 wait = "True"
 ems = Clock.ElapsedMilliseconds
 While wait
  Program.Delay(1000)
  x = 250 + (Math.GetRandomNumber(50) - 25)
  y = 200 + (Math.GetRandomNumber(50) - 25)
  Shapes.Move(sighter, x, y)
  Program.Delay(100)
  Shapes.HideShape(shape[4]["obj"])
  Program.Delay(100)
  Shapes.ShowShape(shape[4]["obj"])
  If 5000 < Clock.ElapsedMilliseconds - ems Then
   wait = "False"
  EndIf
 EndWhile
 Shapes.ShowShape(stair)
 iMin = 1
 iMax = 10
 Shapes_Remove()
 Shapes.Remove(title)
EndSub

図形の描画

撃った弾が当たったかどうかの判定のために作ったサブルーチンです。現在のところこのプログラムでしか利用していませんが、将来のためになるべく汎用的にしてあります。ただし、まだいくつか制限があります。図形の枠には対応していません。長方形と楕円の回転には対応していません。ブラウザ上で実行するときのみ呼び出されます。

Sub Shapes_Draw
 ' Shapes | draw shapes
 ' param iMin, iMax - shape indices to add
 ' param shape - array of shapes
 ' param scale - 1 if same scale
 ' TODO to draw border line for rectangle, triangle and ellipse
 ' TODO to rotate rectangle and ellipse (text?)
 Stack.PushValue("local", x)
 Stack.PushValue("local", y)
 Stack.PushValue("local", i)
 s = scale
 For i = iMin To iMax
  If shape[i]["pw"] > 0 Then
   GraphicsWindow.PenColor = shape[i]["pc"]
  EndIf
  If Text.IsSubText("rect|ell|tri|text", shape[i]["func"]) Then
   GraphicsWindow.BrushColor = shape[i]["bc"]
  EndIf
  x = shX + shape[i]["x"] * s
  y = shY + shape[i]["y"] * s
  If shape[i]["func"] = "rect" Then
   GraphicsWindow.FillRectangle(x, y, shape[i]["width"]* s, shape[i]["height"] * s)
  ElseIf shape[i]["func"] = "ell" Then
   GraphicsWindow.FillEllipse(x, y, shape[i]["width"]* s, shape[i]["height"] * s)
  ElseIf shape[i]["func"] = "tri" Then
   x[1] = shX + shape[i]["x"] * s + shape[i]["x1"] * s
   y[1] = shY + shape[i]["y"] * s + shape[i]["y1"] * s
   x[2] = shX + shape[i]["x"] * s + shape[i]["x2"] * s
   y[2] = shY + shape[i]["y"] * s + shape[i]["y2"] * s
   x[3] = shX + shape[i]["x"] * s + shape[i]["x3"] * s
   y[3] = shY + shape[i]["y"] * s + shape[i]["y3"] * s
   angle = shape[i]["angle"]
   If angle <> 0 Then
    n = 3
    ox = (x[2] + x[3]) / 2
    oy = (y[1] + y[2]) / 2
    Shapes_RotatePolyline()
   EndIf
   GraphicsWindow.FillTriangle(x[1], y[1], x[2], y[2], x[3], y[3])
  ElseIf shape[i]["func"] = "line" Then
   x[1] = shX + shape[i]["x"] * s + shape[i]["x1"] * s
   y[1] = shY + shape[i]["y"] * s + shape[i]["y1"] * s
   x[2] = shX + shape[i]["x"] * s + shape[i]["x2"] * s
   y[2] = shY + shape[i]["y"] * s + shape[i]["y2"] * s
   If angle <> 0 Then
    n = 3
    ox = (x[2] + x[3]) / 2
    oy = (y[1] + y[2]) / 2
    Shapes_RotatePolyline()
   EndIf
   GraphicsWindow.DrawLine(x[1], y[1], x[2], y[2])
  ElseIf shape[i]["func"] = "text" Then
   If silverlight Then
    fs = Math.Floor(shape[i]["fs"] * 0.9)
   Else
    fs = shape[i]["fs"]
   EndIf
   GraphicsWindow.FontSize = fs * s
   GraphicsWindow.FontName = shape[i]["fn"]
   GraphicsWindow.DrawText(x, y, shape[i]["text"])
  EndIf
 EndFor
 i = Stack.PopValue("local")
 y = Stack.PopValue("local")
 x = Stack.PopValue("local")
EndSub

ポリラインの回転

三角形の頂点を回転させるために書きましたが、これも汎用性を持たせるため、ポリラインの頂点を回転できるようになっています。

Sub Shapes_RotatePolyline
 ' Shapes | rotate polyline
 ' param n - number of points
 ' param x, y - array of x and y co-ordinates
 ' param ox, oy, - center of rotation
 ' param angle - angle of rotation
 Stack.PushValue("local", i)
 _a = Math.GetRadians(angle)
 For i = 1 To n
  xi = (x[i] - ox) * Math.Cos(_a) + (y[i] - oy) * Math.Sin(_a)
  yi = - (x[i] - ox) * Math.Sin(_a) + (y[i] - oy) * Math.Cos(_a)
  x[i] = xi + ox
  y[i] = yi + oy
 EndFor
 i = Stack.PopValue("local")
EndSub

自動生成されたコード

これ以降のコードは図形エディタ Shapes 1.5b により生成されたコードです。各サブルーチンの働きについて簡単に説明するだけにしておきます。図形エディタではこの他にも図形を回転するサブルーチンもありますが、使用しないので削除してあります。

  • SB_Workaround - 回避策のためブラウザ上での実行かどうかを判断します。
  • Shapes_Add - 配列 shape のデータから Shapes オブジェクトによる図形を追加します。
  • Shapes_CalcWidthAndHeight - 配列 shape のデータ全体の幅と高さを求めます。
  • Shapes_Init - アヒルの図形データを配列 shape に格納します。
  • Shapes_Move - Shapes_Add で追加した Shapes 図形を移動します。
  • Shapes_Remove - Shapes_Add で追加した Shapes 図形を削除します。

アヒルの射撃ゲームとしては、アヒルをもう少し増やして難易度を上げると面白くなるでしょう。チャレンジしてみませんか?

|

« プログラミング講座(158) 垂直スクロールゲーム(解説その2) | トップページ | プログラミング講座(160) エアホッケー »

Small Basic」カテゴリの記事

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

コメント

コメントを書く



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




トラックバック


この記事へのトラックバック一覧です: プログラミング講座(159) 射撃ゲーム:

« プログラミング講座(158) 垂直スクロールゲーム(解説その2) | トップページ | プログラミング講座(160) エアホッケー »