キーボードイベントの利用

前々回の続きで今回はキーボードイベントを試してみましょう。

グラフィックスウィンドーでキーボードが押された時と離された時にイベントを発生させることができる。キーボードを押した歳のイベントは GraphicsWindow.KeyDown 、キーボードが離された際のイベントはGraphicsWindow.KeyUp でイベントハンドラを割り当てます。

この二つのイベントはどのように使い分けるのでしょうか?

実はGraphicsWindow.KeyDown イベントはキーボードのキーを押したときだけではなく、例えばそのキーを押しっぱなしにしていると定期的にイベントが発生します。ちょっと何の事を言っているか分かりにくいですよね。サンプルプログラムを見てください。

wWidth  = 800
wHeight = 800
cursize = 40
x = wWidth /2  - cursize /2
y = wHeight /2 - cursize /2
GraphicsWindow.Width  = wWidth
GraphicsWindow.Height = wHeight
cur = Shapes.AddEllipse(cursize,cursize)
Shapes.Move(cur, x, y)

GraphicsWindow.Show()
GraphicsWindow.KeyDown = OnKeyDown

Sub OnKeyDown
  If GraphicsWindow.LastKey = "H" Then
    x = x - 10
  ElseIf GraphicsWindow.LastKey = "L" Then
    x = x + 10
  ElseIf GraphicsWindow.LastKey = "J" Then
    y = y + 10
  ElseIf GraphicsWindow.LastKey = "K" Then
    y = y - 10
  EndIf
  Shapes.Animate(cur, x, y, 5)
EndSub

グラフィックスウィンドーが開くと真中に青い円が現れます。キーボードの"h", "j", "l", "k" を押すと青い円が上下左右に移動します。

プログラムを見てみましょう。GraphicsWindow.KeyDown イベントにサブルーチンOnKeyDownを割り当てているのは前々回の説明の流れなので分かると思います。サブルーチンOnKeyDown の中でGraphicsWindow.LastKeyというプロパティをチェックしています。このプロパティはもっとも最近に押されたキーを保持しています。つまり、例えばキーボードから「a」のキーを押すとこのプロパティには"A"という文字が入ります。小文字の「a」を押しても、大文字の「A」を押しても"A"という文字が入ることに気を付けてください。ついでに言うと、「Ctrl-A」を押した場合にも"A"が入ります。

ところで、Windowsの場合キーボードのあるキーを押しっぱなしにした場合、最初の文字が入力されてからほんの少し(0.5秒くらい?)だけまってからその文字が連続されて入力されますよね。「メモ帳」で試してみてください。この仕組みをキーリピートと呼んだりします。

キーを押しっぱなしにした場合、GraphicsWindow.KeyDown で発生するキーボードイベントもこの仕組みのもとで連続した複数のイベントを発生させます。

さて、キーボードイベントにはGraphicsWindow.KeyUp というキーを話した時に発生するイベントもあります。先ほどのサンプルプログラムの12行目の GraphicsWindow.KeyDown を GraphicsWindow.KeyUp に変更してみましょう。プログラムを実行するとどうなるでしょうか。先ほどと同様にキーボードの hjkl キーで青い円が移動しますが、先ほどと違い、キーを押しっぱなしにしても円は一度しか動きません。円を動かすためにはキーをポンポンと押したり離したりする度に円が移動します。GraphicsWindow.KeyUp イベントは実際にキーを話した際にだけ発生することに注意してください。

このあたりの動きがGraphicsWindow.KeyDown と GraphicsWindow.KeyUp イベントの違いです。

ところで補足です。通常の文字以外のキーが押された場合はどうなるのでしょうか。

キー LastKey キー LastKey キー LastKey キー LastKey
F1〜F12 "F1",..,"F12" Tab "Tab" 左側Shift "LeftShift" 左側Ctrl "LeftCtrl"
左側Windowsキー "LWin" Alt "System" スペース "Space" 無変換 "ImeNonConvert"
1〜0 "D1",..,"D0" - "OemMinus" ^ "OemQuotes" \ "Oem5"
[ "OemOpenBlackets" ] "Oem6" + "OemPlus" "Oem1"
, "OemComma" . "OemPeriod" / "OemQuestion" \(_) "OemBackslash"
"Left" "Right" "Up" "Down"
PgDown "Next" PgUp "PageUp" Home "Home" アプリケーションキー "Apps"
右側Ctrl "RightCtrl" 右側Shift "RightShift" Enter "Return" Scroll Lock "Scroll"
Backspace "Back" Del "Delete" Ins "Insert" Pause "Pause"

ところで、今回のプログラムを実行して青い丸を動かし続けると画面の外に出てしまいます。青い丸が外に出ないようにするには画面の(座標の)境界を調べて、境界に青い丸が来た場合にはそれ以上移動しないようにすればよいでしょう。

wWidth  = 800
wHeight = 800
cursize = 40
x = wWidth /2  - cursize /2
y = wHeight /2 - cursize /2
GraphicsWindow.Width  = wWidth
GraphicsWindow.Height = wHeight
cur = Shapes.AddEllipse(cursize,cursize)
Shapes.Move(cur, x, y)

GraphicsWindow.Show()
GraphicsWindow.KeyDown = OnKeyDown

Sub OnKeyDown
  If GraphicsWindow.LastKey = "H" Then
    x = x - 10
    If x < 0 Then
      x = 0
    EndIf
  ElseIf GraphicsWindow.LastKey = "L" Then
    x = x + 10
    If x > wWidth - cursize then
      x = wWidth - cursize
    EndIf
  ElseIf GraphicsWindow.LastKey = "J" Then
    y = y + 10
    If y > wHeight - cursize then
      y = wHeight - cursize
    EndIf
  ElseIf GraphicsWindow.LastKey = "K" Then
    y = y - 10
    If y < 0 then
      y = 0
    EndIf
  EndIf
  Shapes.Animate(cur, x, y, 5)
EndSub

Shapesの利用とアニメーション

Small Basic のグラフィックスにはShapesと呼ばれる面白い機能があります。グラフィックスウィンドーに例えばGraphicsWindow.DrawEllipse() やGraphicsWindow.DrawRectangle()等で描かれた円や長方形は一旦グラフィックスウィンドーに描かれると移動させたりサイズを変えたりといったことはできません。スケッチブックに色鉛筆で絵を描いてしまったようなものですね。

でも例えばゲームをプログラミングしようと思うとそういうグラフィックスを移動させたりしたくなってきますよね。疑似的に動いているように見せることはできます。どうするかと言うと、要は描いたグラフィックスを一旦消して、少し移動した場所に描きなおせばいいのです。例えば下の例で、太陽のマークのグラフィックスを右に移動させたいとします。

まずは、最初の位置の太陽を消すように背景色で塗りつぶします。黒い枠で囲ったあたりを背景色で塗りつぶせばよいですよね。

次に移動したい場所に太陽のグラフィックスを描画します。

これをくりかええばあたかも太陽のグラフィックスが左から右へ移動しているように見えるでしょう。ちなみに離れた位置に再描画すれば素早く、近い位置に再描画すればゆっくりと移動したように見えるでしょう。

では、この仕組みをつかってサンプルプログラムを作ってみました。ちょっと長いですが簡単なゲームになっています。

GraphicsWindow.Width = 800
GraphicsWindow.Height = 800
GraphicsWindow.BackgroundColor = "Black"
cx = 400
cy = 400
earthR=2000
altitude = 400
ground = 600
G = 0.05
speed = 0.0

GraphicsWindow.Show()
DrawStars()
GraphicsWindow.BrushColor = "Blue"
GraphicsWindow.FillEllipse(cx-earthR, ground, earthR*2, earthR*2)
GraphicsWindow.PenColor = "Black"

GraphicsWindow.MouseDown = OnMouseDown

loop:
  GraphicsWindow.BrushColor = "Black"
  GraphicsWindow.FillRectangle(cx-10, ground - altitude, 20, 20)
  If altitude <= 0 Then
    If speed < 2 then
      GraphicsWindow.ShowMessage("無事に着地", "成功")
    else
      GraphicsWindow.ShowMessage("地面に激突", "失敗")
    endif
    Program.End()
  EndIf
  speed = speed + G
  altitude = altitude - speed
  GraphicsWindow.BrushColor = "White"
  GraphicsWindow.FillRectangle(cx-10, ground - altitude, 20, 20)
  Program.Delay(33)
Goto loop

Sub OnMouseDown
  speed = speed - 1.0
EndSub

Sub DrawStars
  For I=0 To 4000
    GraphicsWindow.SetPixel(Math.GetRandomNumber(800), Math.GetRandomNumber(800), GraphicsWindow.GetRandomColor())
  EndFor
EndSub

このゲームはロケットの着陸船を無事に着陸させるゲームです。そのまま放っておくと重力に引かれて次第に落下し、地面に激突してしまいます。ですので、マウスをクリックして着陸ロケットを噴射し、降下速度を落として着陸させる必要があります。まぁ、着陸船は単なる白い正方形だし、もっと凝った作りにもできるかもしれませんが、サンプルなどのこの程度で許してください。

理科で「速度と加速度」や「重力加速度」を習っていれば内容を理解しやすいかもしれませんが、そうでない場合はまぁ、色々計算している部分は適当に流し読みしてしまいましょう。

着陸船の移動ですが、(1)の箇所でブラシの色を背景の黒にして正方形を描いていますが、要はここと次の行(2)で以前に書いた白色の着陸船を消しているのです。(3)の箇所で着陸船の色である白にブラシを選択し、(4)で着陸船を描画しています。これを繰り返すことによりまるでアニメーションのように着陸船が移動しているように見えるのです。

でも、あれ?よく見てみると、着陸船が通った後、背景にあった星が消えて真っ黒になってしまっていますね。これは、当たり前と言えば当たり前で、着陸船を移動する際に単純に黒い正方形で塗りつぶしているからです。このように背景のグラフィックが消えてしまう事を防ぐテクニックももちろんあります。要は着陸船を描画する前に、そこに描画されていたグラフィックスをどこかに記憶しておいて、着陸船を消す代わりに記憶しておいたグラフィックスをつかって描画しなおしてやればよいのです。

でも、なんだか面倒だよね。

Shapesの利用

Small Basicにはこのような場面で利用できるとても便利な機能があります。それが、Shapesです。Shapes は単純な図形を格納する変数のようなものです。Shapesに図形を格納し、そのShapesをグラフィックスウィンドーの好きな位置に移動させたり拡大させたり透明度を変えたりすることができます。ちょうど、スケッチブックにシールを張るようなものです。シールをはがして別の場所に移動させれば、そのシールに描かれた図形は移動するし、スケッチブックのシールが貼ってあった箇所の図形もそのまま残っているはずです。まぁ、シールの糊がそれほど強力でなければね。

Shapesに例えば矩形(長方形)を登録するには、次のようにします。

lander = Shapes.AddRectangle(20,20)

これは、幅20、高さ20の長方形(つまり正方形)をShapesに追加し、これを"lander"という変数に格納しています。別の言い方をすると幅高さそれぞれ20の正方形をShapesとして登録し、それに"lander"という名前を付けているともいえます。

この"lander"という名前の正方形を移動するには次のようにします。

Shapes.Move(lander, x, y)

"lander"はもちろん今回作成したshapesの名前で、あとのxとyはこのShapesを移動するグラフィックスウィンドー上の座標です。

では、このShapesを使って先ほどの着陸ゲームを作り直してみましょう。

GraphicsWindow.Width = 800
GraphicsWindow.Height = 800
GraphicsWindow.BackgroundColor = "Black"
cx = 400
cy = 400
earthR=2000
altitude = 400
ground = 600
G = 0.05
speed = 0.0

GraphicsWindow.Show()
DrawStars()
GraphicsWindow.BrushColor = "Blue"
GraphicsWindow.FillEllipse(cx-earthR, ground, earthR*2, earthR*2)
GraphicsWindow.PenColor = "Black"
GraphicsWindow.BrushColor = "White"
lander = Shapes.AddRectangle(20,20)
GraphicsWindow.MouseDown = OnMouseDown

loop:
  Shapes.Move(lander, cx-10, ground - altitude) 
  If altitude <= 0 Then
    If speed < 2 then
      GraphicsWindow.ShowMessage("無事に着地", "成功")
    else
      GraphicsWindow.ShowMessage("地面に激突", "失敗")
    endif
    Program.End()
  EndIf
  speed = speed + G
  altitude = altitude - speed
  Program.Delay(33)
Goto loop

Sub OnMouseDown
  speed = speed - 1.0
EndSub

Sub DrawStars
  For I=0 To 4000
    GraphicsWindow.SetPixel(Math.GetRandomNumber(800), Math.GetRandomNumber(800), GraphicsWindow.GetRandomColor())
  EndFor
EndSub

ちょっとだけすっきりとしたかな?実際に動かしてみると分かりますが、着陸船が移動しても背景の星々は消えることはありません。

さてこのゲームですが、アイディア次第で色々と改造できると思います。例えば燃料の残量の概念を導入して着陸ロケットの噴射回数、つまりマウスをクリックできる回数を制限してみたり、時間制限を導入してみたり、等など。

試してみるといいでしょう。

Shapesにはもちろん長方形以外の図形も登録できます。工夫してみてください。

イベント発生時のコールバックとしてのサブルーチンの利用

タイトルを見るとオドロオドロシイ感じがするかもしれませんが、まずはサンプルプログラムを見てみましょう。

GraphicsWindow.MouseDown = OnMouseDown                         <=== (1)

Sub OnMouseDown                                                <=== (2)
  x = GraphicsWindow.MouseX
  y = GraphicsWindow.MouseY
  GraphicsWindow.PenColor = GraphicsWindow.GetRandomColor()
  GraphicsWindow.DrawEllipse(x,y,20,20)
EndSub

実行したら、グラフィックウィンドーが開くのでマウスを左クリックしてみよう。

プログラム自体はとてもシンプルで、メインの処理が一行(1)と、サブルーチンの定義(2)があるだけです。しかも、(1)ではサブルーチンの名前をGraphicsWindow.MouseDown にセットしているだけで、このサブルーチン自体を呼び出してはいません。ちなみに前回、サブルーチンの定義を行っている部分(Sub からEndSubまで)はメインの処理の流れのなかでは無視されることを説明しました。しかし、このプログラムを実行すると明らかにサブルーチンOnMouseDownの処理が何度か、しかもマウスボタンをクリックした際に実行されることがわかると思います。しかし、このサブルーチンOnMouseDownを呼び出してる箇所は実際にはどこにも見当たらないことに気づくと思います。

Small Basicのプログラムは書かれた順に実行されます。プログラムの流れを指で辿るように追ってゆくことができます。しかし例外があります。なにか特別な状況が発生した際にはプログラムの通常の流れとは外れたところで処理の流れに割り込みをかけて、その状況のための特別な処理を実行することができます。このような「特別な状況」のことを「イベント」と呼び、イベントを処理するプログラムの部分を「イベントハンドラ」と呼びます。ハンドラ、とは「処理する者」位の意味です。

あまりいい例えではないかもしれませんが、テレビの番組を放送中に重大事件が発生すると番組をいったん中断し、その事件についての報道を行った後もう一度番組に戻ってくることがありますよね。この場合、重大事件がイベント、重大事件の報道をイベントハンドラに相当します。重大事件はいつ発生するか分からず予定の立っているものではありません。また、重大事件の報道が終わると、通常の番組の中断された箇所に戻り番組が再開されるでしょう。

イベントとイベントハンドラの関係もそれに似ています。

でも、イベントとは具体的にどんなものなのでしょうか? Small Basic では幾つものイベントが提供されています。例えば、マウスのボタンが押された時、キーボードからキーが押された時等などにイベントが発生します。イベントハンドラはサブルーチンの形で定義します。そして、イベントハンドラ(サブルーチン)をイベントに代入することでイベントとイベントハンドラを関係づけることができます。

先ほどのサンプルプログラムの場合、(1)がイベントとイベントハンドラを関連付けている箇所になります。GraphicsWindow.MouseDown はマウスボタンが押された瞬間に発生するイベントです。ちなみに、GraphicsWindow.MouseUpというイベントもあり、こちらはマウスボタンが離された瞬間に発生します。

ところでイベントに対してイベントハンドラを複数関連付けた場合どうなるのでしょうか?

GraphicsWindow.MouseDown = OnMouseDown1                               <==== (1)
GraphicsWindow.MouseDown = OnMouseDown2                               <==== (2)

Sub OnMouseDown1
  GraphicsWindow.PenColor=GraphicsWindow.GetRandomColor()
  GraphicsWindow.BrushColor=GraphicsWindow.GetRandomColor()
  GraphicsWindow.FillEllipse(GraphicsWindow.MouseX, GraphicsWindow.MouseY,50,50)
EndSub
  
Sub OnMouseDown2
  GraphicsWindow.PenColor=GraphicsWindow.GetRandomColor()
  GraphicsWindow.BrushColor=GraphicsWindow.GetRandomColor()
  GraphicsWindow.FillRectangle(GraphicsWindow.MouseX, GraphicsWindow.MouseY,20,20)
EndSub

(1)と(2)でGraphicsWindow.MouseDownというイベントに対し、イベントハンドラが二回定義されています。実行すると分かりますが、イベントに対し複数回イベントハンドラが関連付けられた場合、最後に関連付けられたイベントハンドラが有効になります。この場合、(2)で関連付けられたサブルーチン、OnMouseDown2ですね。

その他のイベント

他にどのようなイベントがあるのでしょうか?ここに列挙したものはSmall Basic で提供される全てではありませんが、以下に主なイベントをリストしてみました。

マウスイベントのイベント
GraphicsWindow.MouseDown
マウスボタンが押されたときに発生するイベント
GraphicsWindow.MouseUp
マウスボタンが離されたときに発生するイベント
GraphicsWindow.MouseMove
マウスカーソルが移動した際に発生するイベント
グラフィックスウィンドーでのキーボードのイベント
GraphicsWindow.KeyDown
グラフィックスウィンドーでキーボードのキーが押されたときに発生するイベント
GraphicsWindow.KeyUp
グラフィックスウィンドーでキーボードのキーが離されたときに発生するイベント
グラフイックスウィンドーでテキストを入力した際のイベント
GraphicsWindow.TextInput
グラフィックスウィンドーでテキストが入力されたときに発生するイベント
タイマーイベント
Timer.Tick
前もってTimer.Interval に対して定義された時間間隔(ミリセカンド[1/1000秒]単位で指定)ごとに発生するイベント

マウスイベントを利用して絵を描いてみる

さて、では試しにマウスイベントを利用して絵を描く簡単なプログラムを作ってみましょう。まずは、どういう方針で作るか考えてみます。例えば…マウスカーソルが動いたときにその座標に点を打ってみることにします。とは言っても、マウスカーソルが動いた場合常に点を打つのではなく、やはり例えば左ボタンが押されている間だけ描画したいので、Mouse.IsLeftButtonDown というプロパティを使ってみます。

GraphicsWindow.MouseMove = OnMouseMove

Sub OnMouseMove
  If Mouse.IsLeftButtonDown Then
    GraphicsWindow.SetPixel(GraphicsWindow.MouseX, GraphicsWindow.MouseY, "Black")
  EndIf
EndSub

Mouse.IsLeftButtonDown はマウスの左ボタンが押されているかどうかを調べるためのプロパティです。"Is Left Button Down ?(左ボタンが押下されている?"位の意味ですね。試しに実行してみましょう。

うーん...正直微妙。

練習問題として試しに作ってみるとよいでしょう。ヒントを幾つか出しておきます。マウスが移動する直前の座標を記憶しておいて、マウス移動後現在の座標と記憶しておいた座標の間で線を引くとそれらしくなります。こんな感じ。

例を置いておきますが、一応隠しておきますね。

続きを読む

サブルーチン

少し長めのプログラムを作っていると、同じような手続きの繰り返しがそこかしこに出てくることがよくあります。このような場合、「サブルーチン」を使うと便利です。サブルーチンは手続きに名前をつけたもので、あたかも新たなSmall Basicの命令が作られたようなイメージで使う事ができます。

例を見てみましょう。このサンプルプログラム自体はあまり意味のあることをやっているわけではありません。

debug=1

GraphicsWindow.Height=600
GraphicsWindow.Width=600
GraphicsWindow.BackgroundColor="darkgreen"
GraphicsWindow.Show()
GraphicsWindow.PenColor="white"

For x=0 to 600 step 40
  For y=0 To 600 Step 40
    DebugOutput()                                   <=== (1)
    GraphicsWindow.DrawRectangle(x,y,30,30)
    y=y+2
    DebugOutput()                                   <=== (2)
  EndFor
  x=x+2
EndFor

Sub DebugOutput                                     <=== (3)
  If debug = 1 Then
    TextWindow.Write("DBG: Time: "+Clock.Time+" ")
    TextWindow.WriteLine("x="+x+", y="+y)
  EndIf
EndSub                                              <=== (4)

(3) の行で新しいキーワード"Sub"を使い、「Sub DebugOutput」という名前が定義されています。この(3)から(4)の"EndSub"まででサブルーチン "DebugOutput"を定義しています。このサブルーチンでやっていることは単純なので見ればわかると思いますが、変数 debug というのが1だった場合に、その時の時刻と変数x,yの値をテキストウィンドーに表示しています。

この手続き(サブルーチン)を実際に使っているのが(1)と(2)の行です。(1)の行が実行されると、Goto 文と似たように処理がDebugOutput() が定義されている(3)に飛びます。そして、Sub とEndSub で囲まれた(3)〜(4)の手続きが実行され、次にこの手続きを呼び出した(1)の行に自動的に戻ってきて次の行の実行を行います。ここがGoto文とサブルーチンの違いで、またサブルーチンの便利なところです。Goto文で同じことをしようとしても、帰り先をラベルで明示的に指定してやらなければいけませんし、また帰り先を呼び出しの度に変更するという事もできません。

もちろんここに(3)と(4)に挟まれた手続きを(1)や(2)の位置にコピー&ペーストでタラタラと書き流してもよいのだけど、それだと更に同じ処理が追加で何か所にも出てくると面倒になってくると思います。例えば、この例では日付は表示せずに時刻だけを表示していましたが、何かの理由で日付も表示したくなったとします。100個所にコピー&ペーストしていた場合、100個所を修正しなくてはいけないし、たぶんそのうちに修正漏れやミスが発生すると思います。サブルーチンを使った場合、たった一か所だけを修正するだけですみます。

ところで試しに上の例の(1)と(2)の行を取り去って、プログラムの最後に一行追加してみましょう。

debug=1

GraphicsWindow.Height=600
GraphicsWindow.Width=600
GraphicsWindow.BackgroundColor="darkgreen"
GraphicsWindow.Show()
GraphicsWindow.PenColor="white"

For x=0 to 600 step 40
  For y=0 To 600 Step 40
    GraphicsWindow.DrawRectangle(x,y,30,30)
    y=y+2
  EndFor
  x=x+2
EndFor
                                                    <=== (A)
Sub DebugOutput                                     <=== (3)
  If debug = 1 Then
    TextWindow.Write("DBG: Time: "+Clock.Time+" ")
    TextWindow.WriteLine("x="+x+", y="+y)
  EndIf
EndSub                                              <=== (4)

GraphicsWindow.ShowMessage("hi", "hi")              <=== (B)

この時、サブルーチンDebugOutoutを呼び出している個所はどこにもありません。ですので、プログラムが実行されてもテキストウィンドーは開かれず、なにもテキストは表示されません。また、プログラムは(B)を実行しメッセージボックスを表示します。
なにを言いたいかというと、プログラムの処理が(A)の位置に達しても(3)から(4)の間のサブルーチンの行はあたかもなにもなかったように無視され、そのまま(B)の処理へ移るという事です。この動きもGoto文とは違いますね。

プログラムを見やすくするためにサブルーチンを使う

さて、前節では同じ手続きが何か所も実行される場合に手続きに名前を付けてプログラムを簡略化するためにサブルーチンを使う方法を紹介しました。しかし、実際には一回しか実行されないような処理にもサブルーチンが使われることが多くあります。これは、まとまった手続きに名前をつけることにより全体のプログラムの流れを把握しやすくするために役立ちます。もう一度先ほどのサンプルプログラムを見てみましょう。

debug=1

GraphicsWindow.Height=600                        <=== (1)
GraphicsWindow.Width=600                         <=== (2)
GraphicsWindow.BackgroundColor="darkgreen"       <=== (3)
GraphicsWindow.Show()                            <=== (4)
GraphicsWindow.PenColor="white"                  <=== (5)

For x=0 to 600 step 40
  For y=0 To 600 Step 40
    DebugOutput()
    GraphicsWindow.DrawRectangle(x,y,30,30)
    y=y+2
    DebugOutput()
  EndFor
  x=x+2
EndFor

Sub DebugOutput
  If debug = 1 Then
    TextWindow.Write("DBG: Time: "+Clock.Time+" ")
    TextWindow.WriteLine("x="+x+", y="+y)
  EndIf
EndSub

Small Basicでグラフィックスプログラミングをやっていくと、このサンプルの(1)〜(5)の処理、つまりグラフィックスウィンドーの体裁を定義するという処理は何度も出てくるパターンのようなものだという事に気づくと思います。

これを、次のように変えてみましょう。

debug=1

InitGraphicsScreen()                 <==== (1)
DrawRectangles()                     <==== (2)

'=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

Sub InitGraphicsScreen               <==== (3)
  GraphicsWindow.Height=600
  GraphicsWindow.Width=600
  GraphicsWindow.BackgroundColor="darkgreen"
  GraphicsWindow.Show()
  GraphicsWindow.PenColor="white"
EndSub

Sub DrawRectangles                    <==== (4)
  For x=0 to 600 step 40
    For y=0 To 600 Step 40
      DebugOutput()
      GraphicsWindow.DrawRectangle(x,y,30,30)
      y=y+2
      DebugOutput()
    EndFor
    x=x+2
  EndFor
EndSub

Sub DebugOutput
  If debug = 1 Then
    TextWindow.Write("DBG: Time: "+Clock.Time+" ")
    TextWindow.WriteLine("x="+x+", y="+y)
  EndIf
EndSub  

サブルーチンInitGraphicsScreenは画面の初期化を行うだけなので最初に一回だけ実行されます。何度も実行される処理ではないでしょう。また、サブルーチンDrawRectanglesも同様です。でも、敢えてサブルーチン化しているます。このプログラム、実質的には手続きは(1)と(2)の二つの行で定義されていることがわかるでしょうか。このプログラムでやりたかったことのアウトラインは、

  1. 画面を初期化し、
  2. 長方形(正方形)をたくさん描画する

という二つの事だけです。この二つをどう具体的に行うかは、別途考えてもよいことなのです。ある意味、ここではそれぞれの処理を「抽象化」したとも言えます。大きなプログラムを作る場合、処理の大まかな流れを最初に考えて、そのあとディテールを考えていくというトップダウンのやり方をを取るほうがプログラムの設計がしやすいことが多いのです。

例えば50階建のビルの設計をするとして、たぶん最初に行うのは大体の外観をデザインして、それから少しづつ詳細の設計に入っていくと思います。最初からトイレの電気の配線の設計の様なディテールを考える建築士はいないはずです。

また、この事にはもう一つの面があって、処理が抽象化されているので後からそのプログラムを見た人が全体の流れを把握しやすいという事が利点があります。プログラムを見る人は最初に大雑把な流れをつかんでからディテールを見たいでしょう。「ふむふむ、最初に画面を初期化して、その後に長方形を描画するのだな。では、画面の初期化は具体的には、ふむふむこうやっているのか…」と言った感じ。

ところでちょっと補足しておくと、ここで言っている「後からプログラムを見る人」とは別に赤の他人だけとは限りません。「未来の自分」かもしれません。例えばプログラムを作ってから1年もたってからそのプログラムを見直す必要ができたとき、プログラムの詳細まで覚えている人はどれだけいるでしょう。未来の自分は十分に赤の他人と言ってもいいのです。

まとめ

ここではサブルーチンを使う目的として次の二つを紹介しました

  1. 同じような手続きに名前をつけ、ひとまとまりの処理として扱う
  2. 手続きを抽象化しプログラムを見やすくする

実際にはその他の目的でもサブルーチンが使われることがあります。次回は「イベント発生時のコールバック」としての使い方を紹介します。今はなんのこっちゃ分からないと思うけど、心配しないください。

ところでSmall Basicの他のプログラミング言語でもサブルーチンやそれに似た言語機構を持っていて、それらは関数だとかメソッドだとか別の名前で呼ばれることがあります。それらについてはいずれ機会があれば説明したいと思います。

乱数について

前回のモンテカルロ法について少し補足です。

改めてリファレンスマニュアルを読むと、Small BasicのMath.GetRandumNumber()は少し気をつけて使わないといけないようです。

Math.GetRandomNumber(maxNumber)

maxNumber: 要求された乱数値の最大値。
戻り値:    指定された最大値よりも小さいか等しい乱数。

つまり、例えば引数として100を渡した場合、1〜100までの乱数を整数で返す、という仕様のようです。これは乱数としてはモンテカルロ法のために使うには少し性能面で問題がありそうです。気をつけて使わないといけません。

Goto 文を使った繰り返し

さて、色々あって前回の記事から一年ほど開いてしまいました。
その間に地震があったり、Small Basci のバージョン1.0がリリースされたりと様々な事が起きました。
Small Basci 日記、再開したいと思います。

前回、For 文を使った繰り返しについて説明しました。For 文は予め繰り返しの回数や条件がはっきりしている場合にとても便利に使えますが、逆に条件がはっきりしない、不定だったり、そもそも回数を決めないで繰り返しを行いたい場合には別の分を使ったほうがよい場合があります。
そこで今回はGoto 文を使った繰り返しの方法を説明したいと思います。

Goto 文は、処理の流れを変えるための命令です。

通常プログラムの処理の流れは書かれた順に沿い、プログラムの最初から最後に至るまで順番に実行されます。これはある意味戯曲の台本や、音楽の楽譜の流れと同じです。ただ、以前に紹介したように通常プログラミング言語には処理の流れを変える方法、言い換えれば命令が備わっています。それが、前回までに説明した For 文による繰り返しであり、If文による条件分岐です。

Small Basci にはFor 文やIf 文以外にも処理の流れを変えるための命令がいくつかあり、Goto 文はその中の一つです。

Goto 文は、英語の"Go to"、つまり「〜へ行け」という文に由来します。Goto 文はパラメータとして「ラベル」を一つ指定してやる必要があります。ラベルとはプログラマーが自由に定義することのできるキーワードで、Goto文での飛び先を指定します。

Goto label     <<-----(1)
  y = 10       <<-----(2)
label:
  y = 20       <<---- (3)
 x = 60       <<-----(4)

この例では(1)でラベル"label"を指定してGoto文が支持されています。そのため、Goto文が実行されると、次に実行されるのはGoto文の次の行、(2)の命令ではなく、ラベル"label"で指定された飛び先の次の行、つまり(3)からとなります。(3)が実行された後は、通常通り(3)の次の行、(4)が実行され、そのまま順に処理が続きます。
ところで、ラベル"label"はお尻にコロン(:)をつける必要があることに注意してください。これは、このキーワードがラベルだということを表すための、まぁ約束事のようなものです。ラベルは、Goto での飛び先の目印だと覚えておいてもらえばいいと思います。

でも、処理の流れを変えるとなにが嬉しいのでしょうか? Goto 文はとても自由度が高い、ある意味強力な命令です。色々な使い方ができるのだけど、特に「ループ(繰り返し)」と、「大域脱出」に使われることが多いと思います。「大域脱出」についてはちょっと難しくなるので、これは別の機会に説明したいと思います。なので、今日は「ループ(繰り返し)」について説明しようと思います。

次の例を見てください。

main:                          <---- (1)
  Shapes.Move(bar1, x1, y1)  <---- (2)
  Shapes.Move(bar2, x2, y2)    <---- (3)
Goto main                      <---- (4)
Program.End()                  <---- (5)

ラベルは自由にプログラムの中のどこにでも置くことができます。それは、そのラベルを指定しているGoto文の前であっても後ろであってもかまいません。ただし、同じ名前のラベルを複数置くことはできないので注意してほしい。Goto で飛ぶときに、どちらのラベルに飛べばよいのか分からなくなるからね。

さて上の例の場合、(4)の位置のGoto 文が(1)の位置のラベル(main)を指定しています。この場合、(2)、(3)と実行してから(4)のGoto文で(1)に実行が移るので、結果もう一度(2)、(3)が実行されます。そのあともう一度(4)のGoto文に来るのだけど、またまた(1)のラベル(main)に飛んで、(2)、(3)が実行されて、再度(4)のGoto文に来るのだけど、この流れが永遠に繰り返されることがわかるだろうか?
つまり、(1)と(4)で囲まれた命令が永遠に(無限に)繰り返されるわけです。このような処理の流れを「無限ループ」と呼ぶことがあります。
じゃぁ、この繰り返しはいつ終わるのだろうか?答えは、決して終わらない、です。もっと正確に言えば、外部からプログラムが停止されない限りプログラムは永遠に実行され続けます。外部からプログラムを停止するには、テキストウィンドーが開かれている場合はCtrl-C(Ctrl キーを押しながら c を押す、もしくはグラフィックウインドーが開かれている場合はウィンドウーの閉じるボタン(×ボタン)を押すとよいでしょう。または、Small Basic の実行ウィンドーの中の「プログラムの終了」ボタンを押してもよいです。

では、ちょっと例として前回作ったランダムに色を変えて円を描くプログラムを無限ループさせてみましょう。前回の例ではFor 文を使って100回という回数を切ってループさせていましたが、これを永遠に実行し続けるように改造します。どこを変えたかは、以前のサンプルプログラムと比較して調べてみてください。

WIDTH=640
HEIGHT=400
RR=50
GraphicsWindow.Width=WIDTH
GraphicsWindow.Height=HEIGHT
loop:
  x=Math.GetRandomNumber(WIDTH)
  y=math.GetRandomNumber(HEIGHT)
  r=math.GetRandomNumber(RR)+20
  GraphicsWindow.BrushColor=GraphicsWindow.GetRandomColor()
  GraphicsWindow.FillEllipse(x,y,r,r)
Goto loop

これを実行すると、こんな感じ。


モンテカルロ法

さて、ちょっと脱線してみましょう。

今、縦横それぞれ2mの正方形が地面に描かれているとして、そこにたくさんの小石をばらまいて見ることを想像してください。小石は正方形の中に特にどこかに隔たることなく、満遍なく程よく乱雑にばらまかれるとしましょう。さて、次にこの正方形の中に、ぴったりと入る半径1mの円を描いてみましょう。この場合、この円の中に入っている小石の数は、元の正方形の中の小石の数に比べてどの程度入っていることが期待できるでしょうか?

まだ数学で「確率」や「統計」というのをちゃんとは習っていないかもしれないけれど、直感でも正方形、および円の中に入っている小石の数の比はそれぞれの面積の比に近いものになるのじゃないか、と想像できると思います。そして、その想像はそう間違っているわけではないのです。

ちなみに、
正方形の面積: 2 \times 2 = 4 m^2
円の面積: \pi \times 1 \times 1 = \pi m^2

仮に、正方形全体に入った小石の数をa、円の中に入った小石の数をそのうちのbとすると、次の式が成り立つはずです。ちなみに\simeqは≒と同じ意味で、ほぼ等しいという事を表しています。これは、この式が確率試行を基にしているからです。
 a:b \simeq 4:\pi
a\pi \simeq 4b
\pi \simeq 4b/a

これは何を意味しているかというと、地面に描いた正方形と円の中に小石をばらまくことにより、円周率πの値、少なくともそれに近い値を求めることができるのではないか、という事なのです。では、小石をたくさん集めて実験をしてみましょう。ただ、実際には小石を100個投げるのも、一個の小石を100回投げるのも本質的な違いはないはずだから、たくさんの小石を集める手間を考えると、一個の小石を何回も投げてみるほうが効率はよいでしょう。

でも、本当に何百回も小石を投げてみるのか…?

こういう単純な作業を延々と繰り返すのはコンピュータの得意とするところなので、コンピュータにやらせるのはよいアイディアと言えるでしょう。とは言っても、実際に小石を投げる必要はない。仮想的な小石で十分です。ということで、サンプルプログラムが以下。

offset = 50
r = 500
cx=r/2
cy=r/2
inCount=0
count=0

GraphicsWindow.Width = r + offset * 2
GraphicsWindow.Height = r + offset * 2
GraphicsWindow.Show()

GraphicsWindow.DrawRectangle(offset,offset,r,r)
GraphicsWindow.BrushColor="Pink"
GraphicsWindow.FillEllipse(offset, offset, r,r)

Loop:
  x = Math.GetRandomNumber(r)
  y = Math.GetRandomNumber(r)

  distance = Math.SquareRoot((x-cx)*(x-cx)+(y-cy)*(y-cy))
  If(2*distance < r) Then
    inCount = inCount + 1
  EndIf
  count = count + 1
  GraphicsWindow.SetPixel(x+offset, y+offset, "Black")
  pi=4*inCount/count
  
  TextWindow.CursorLeft=0
  TextWindow.CursorTop=0
  TextWindow.Write("trial "+count+" ")
  TextWindow.WriteLine("PI="+pi)
Goto Loop

これを実行すると黒枠の正方形とピンクの円を描き、その中に小石(点)を打ってゆきます。背景にうっすらとピンク色の円が見えるでしょうか?

ちょっと補足しておくと、円の中心の座標つまりこのサンプルプログラムでは(cx, cy)から点(x,y)までの距離を求めるには距離の公式をつかうとよいでしょう。
 Distance = \sqrt{(x-cx)^2+(y-cy)^2}
距離の公式をもしまだ習っていないのであれば、ピタゴラスの定理を思い浮かべるとよいでしょう。

上は約500,000回小石を投げたに相当する試行を行った結果なのだけど、ではシミュレーションの結果はというと、例えば次のようになります。

概ね、3.14に近い値になっているのがわかるかと思います。50万回も試行して3桁程度の値しか求められないのでちょっとがっかりかもしれないけど、このようなコンピュータを使ったシミュレーションの手法を「モンテカルロ法」と言います。モンテカルロとはカジノで有名なモナコ公国の地区の名前です。つまり、カジノでサイコロを振るようにしてシミュレーションをするところから名づけられた手法なのです。モンテカルロ法は解析的に計算で求めることができないような数値計算の代わりに近似値を求めるためのシミュレーションとして実際に工学等で使われている方法です。

このサンプルプログラムはイメージが湧きやすいようにグラフィックスを使ってどう石が投げられているかを視覚的に見せたけれども、シミュレーションそのものには別にグラフィックスは必須ではありません。また、正方形の中の円を考えるよりも、正方形の中の一つの角を中心とすると1/4円の扇型を考えたほうがプログラムはシンプルになります。なぜなのか、は自分で考えてみると面白いと思います。

色々と最適化してみたのが次のサンプルプログラムです。

r = 1000
rr=r*r
inCount=0
count=0

Loop:
  x = Math.GetRandomNumber(r)
  y = Math.GetRandomNumber(r)

  If((x*x+y*y) < rr) Then
    inCount = inCount + 1
  EndIf
  count = count + 1
  pi=4*inCount/count
  
  TextWindow.CursorLeft=0
  TextWindow.CursorTop=0
  TextWindow.Write("trial "+count+" ")
  TextWindow.WriteLine("PI="+pi)
Goto Loop

このサンプルでは敢えて平方根を求めるMath.SquareRoot()を使うのを避け、二乗のまま比較をするようにしています。何故かというと、一般にコンピュータは平方根を求めるのは非常に時間がかかるので、平方根の計算をするのを避けているためです。とは言っても、これはほかの演算、例えば乗算と比較しての事なので実際にはそれなりに高速に計算します。もう一つの理由としては、平方根を求めることにより演算の誤差が増えていくことを避けるということもあります。

実行結果は、こんな感じ。これは100万回程試行してみた結果です。

なんだか先ほどよりも精度が悪くなった気もするけど、これはSmall Basic が返す乱数が本当の乱数ではなく数学的に求められた擬似的な乱数であることとそれにより必ずしも均質な乱数でないことなどが関係していると思います。

さて、今回のサンプルプログラムではGoto文を使った無限ループを使って実装してみました。もちろん最初から100万回試行してみる、と決めているのであればForループを使うほうが正確だし確実です。ただ、今回のサンプルの場合は最初にどのくらい試行を行うか、言い換えるとループを回すかを決めておらず、その時の気分で回してみるつもりで作ったので、まぁ、そういうわけで無限ループを使ってみました。

まぁ、そういうやり方もあるということで。

スパゲッティプログラム

Goto文を使うことで処理の流れを変えることができることが分かったと思います。けれども、処理の流れを変えるのは慎重に考えて行ってください。次のサンプルプログラムを見てください。これは、円周率πを求めるサンプルを少しいじったものなのだけど、Goto文とラベルがちりばめられたものになっています。たぶん慣れないうちは処理の流れを一つ一つ追っかけているうちに、何が何だか分からなくなってくると思います。

Goto init
main:

Loop:
  x = Math.GetRandomNumber(r)
  y = Math.GetRandomNumber(r)

  If((x*x+y*y) < rr) Then
    inCount = inCount + 1
  EndIf
  count = count + 1
  pi=4*inCount/count
  
  Goto setcusor
  label1:
  TextWindow.Write("trial "+count+" ")
  TextWindow.WriteLine("PI="+pi)
  If(count > 10000) Then
    Goto leaveLoop
Goto Loop
leaveLoop:
Program.End()

init:
r = 1000
rr=r*r
inCount=0
count=0
Goto main

setcusor:
TextWindow.CursorLeft=0
TextWindow.CursorTop=0
Goto label1

このようにGoto文で処理の流れがあっちに行ったりこっちに行ったりするプログラムの事をスパゲッティプログラムと呼ぶことがあります。スパゲッティって、麺がお皿の中でこんがらがってて、一本の麺を最初から最後まで目で追っかけるのはとてもじゃないけどできないよね。スパゲティプログラムというのも同じで、まるでスパゲッティの麺のようにこんがらがっているところから名づけられました。

Goto文は確かに便利なのだけど、間違えた使い方をすると後で見たときに処理の流れを追いかけるのがとても大変になったりすることがあります。昔…三十年くらい前の人たちはいっその事Goto文の使用を禁止するか、文法から取り除いたほうがよっぽどいいんじゃないか、という議論をしていたことがあります。さすがにそれも極端だし、気をつけて使えば便利な事に変わりはないのだから、注意して使うようにしましょうということに結局はなりました。

それで、比較的安全でこんがらかることもない使い方としては、これは人により考え方が違うところもあるのだけど、ループと大域脱出にだけ使うのこと、と考える人もいます。

まぁ、その辺りはもう少しプログラミング慣れてから自分で考えてみるとよいでしょう。

まとめ

今日はGoto文を使って処理の流れを変えること、Goto文を使って無限ループを作る方法についてまとめてみました。
また、スパゲッティプログラムの危険性についても説明しました。

1から10まで足してみよう

期末試験お疲れ様でした。ま、終わったばかり直ぐにお説教をするのはやめておきます(笑)

さて、試験が終わったばかりで問題を解くのも嫌気がさしているかもしれないけど、そこは気にしないで、ここで問題を出してみます。1/31の記事ではいろいろなコンピュータ言語で1から10まで足し合わせるプログラムを紹介しました。

では、Small Basicではどうすればよいだろう?自分でプログラムを作ってみてください。分からないところは悩みすぎる前に尋ねてください。

あと、ガウスのやりかた、つまり10*(1+10)/2 の式を使うのは禁止です。もちろん、自分で計算していきなり55とテキストウィンドーに出力するのもダメ

チャレンジ.1

キーボードから数を入力して1からその数まで足し合わせるプログラムを作りなさい。つまり、キーボードから100と入力したら1から100まで足し合わせてその結果を出力するプログラムです。

チャレンジ.2

キーボードから最初の数と終わりの数の二つを入力して、最初の数から終わりの数までを足し合わせて表示するプログラムを作りなさい。つまり、キーボードから最初に55、次に101と入力したら55から101まで足し合わせた結果を表示するプログラムです。

チャレンジ.3

チャレンジ2で作ったプログラムを実行したときに、最初の数の方が終わりの数よりも大きな数を入力したときに、そのプログラムはどういう動きをしますか?どういう動きをするべきだと思いますか?仮に最初の数のほうが終わりの数よりも大きかった場合、二つの数を入れ替えて小さな数から大きな数まで足し合わせるプログラムを作ってください。

チャレンジ.4

キーボードから数を入力し、0からその数、またはその数が奇数であった場合はその数よりも一つ小さな偶数まで足し合わせるプログラムを作ってください。






チャレンジ1から4で入力される数はすべて整数であると仮定してください。