2024年04月19日くいなちゃん


プログラミング言語Kuin」の基本機能を学ぶ、実行環境がexe用のチュートリアル5です。 今回は、地味ながらもゲーム制作では重要なメニュー画面とセーブ機能を作ってみます。

1シーケンスの設計

1.1シーケンスの作成



ゲームは普通、本編のほかにも、タイトル画面やメニュー画面などいくつものシーンで構成されています。 これらのシーンがどういう順番でどう切り替わるかを「シーケンス」といいます。 プログラムを書く前に、まずはシーケンスを考えてみましょう。
今回作るメニュー画面は、図1-1のようなシーケンスで作ることにしました。
メニュー画面のシーケンス
図1-1: メニュー画面のシーケンス
まずは大まかな流れになっている「フェードイン、カーソル選択、フェードアウト」というシーケンスをプログラムで書いてみます。 前回と同様、スニペットから「mainとdrawコントロール」を選んでから、図1-2のコードを追加します。
  1. enum Sequence
  2.   fadeIn
  3.   menu
  4.   fadeOut
  5. end enum
  6.  
  7. var sequence: @Sequence
  8.  
  9. var wndMain: wnd@Wnd
  10. var drawMain: wnd@Draw
  11.  
  12. func main()
  13.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  14.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  15.  
  16.   do @sequence :: %fadeIn
  17.  
  18.   while(wnd@act())
  19.     switch(@sequence)
  20.     case %fadeIn
  21.     case %menu
  22.     case %fadeOut
  23.     end switch
  24.  
  25.     do draw@render(60)
  26.   end while
  27. end func
図1-2: menu1.kn
1行目から7行目でシーケンスの列挙型と変数を作成し、16行目で初期化し、メインループ中の19行目から23行目ではswitchブロックで、シーケンスに応じた処理をするために分岐しています。
シーケンスだけで中身が無いため、実行しても何も起こりませんが、とりあえず大まかな流れはできました。

1.2フェードイン、フェードアウト



「フェードイン」と「フェードアウト」の中身を実装します。
自前でフェードの効果を実装しても良いですが、既に「wipe」ライブラリにフェードを簡単に行う仕組みが用意されているため、それを利用します(図1-3)。
  1. enum Sequence
  2.   fadeIn
  3.   menu
  4.   fadeOut
  5. end enum
  6.  
  7. var sequence: @Sequence
  8.  
  9. var wndMain: wnd@Wnd
  10. var drawMain: wnd@Draw
  11.  
  12. func main()
  13.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  14.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  15.  
  16.   do @sequence :: %fadeIn
  17.   do wipe@set(%fade, true, 60, draw@black)
  18.   do draw@clearColor(draw@white)
  19.  
  20.   while(wnd@act())
  21.     switch(@sequence)
  22.     case %fadeIn
  23.       if(wipe@draw())
  24.         do @sequence :: %menu
  25.       end if
  26.     case %menu
  27.       do @sequence :: %fadeOut
  28.       do wipe@set(%fade, false, 60, draw@black)
  29.     case %fadeOut
  30.       if(wipe@draw())
  31.         ret
  32.       end if
  33.     end switch
  34.  
  35.     do draw@render(60)
  36.   end while
  37. end func
図1-3: menu2.kn
17行目、28行目の「wipe@set」関数で、フェードの開始を設定します。 括弧内は順に、「ワイプの種類」「trueならイン、falseならアウト」「完了までのフレーム数」「色」です。
初めは23行目から25行目の「%fadeIn」のシーケンスが実行されますが、23行目の「wipe@draw」関数ではフェードの効果が描画されます。 フェードが完了すると、wipe@draw関数の戻り値はtrueを返すため、24行目でシーケンスを次の「%menu」に切り替えます。
27行目、28行目の「%menu」のシーケンスでは、27行目で直ちに次のシーケンス「%fadeOut」に変更し、28行目でフェードアウトを開始させています。 30行目から32行目の「%fadeOut」のシーケンスでは、フェードアウトが終わり次第、31行目の「ret」で終了しています。
背景が黒だとフェードの効果が分からないため、18行目の「draw@clearColor」関数で背景色を白に設定しています。 実行すると、フェードインして、メニュー操作を一瞬で抜けて、フェードアウトして終了することが確認できます(図1-4)。
フェード
図1-4: フェード

2メニュー画面の作成

2.1アレンジャー



さて、ここからはメニュー画面を作成しますが、今回はKuinの視覚的に配置できる機能を使ってみます。 図2-1の流れになります。
アレンジャーの流れ
図2-1: アレンジャーの流れ
メニュー画面を視覚的に作成すると、Kuinのプログラムが自動生成されるという流れです。
この機能を使うには、Kuinエディタの右下のほうにある「追加」をクリックし、新しい「.kn」ファイルを追加します(図2-2)。
アレンジャーの使い方1
図2-2: アレンジャーの使い方1
図の(1)を押すと出てくるダイアログから、(2)の「2D描画」を選択し、(3)で「menu」とファイル名を入力し、(4)の「追加」のボタンを押すと「.kn」ファイルが追加されます。
追加した画面が図2-3です。
アレンジャーの使い方2
図2-3: アレンジャーの使い方2
(1)で「.kn」ファイルが切り替えられます。 (2)で画面の設定ができます。
今回のプログラムは画面のサイズが1600×900なため、(2)の「位置」を「0×0-1600×900」に設定しておきます。 また(2)の「ズーム」の「-」ボタンを押して「0.5」にしておくと、Kuinエディタ上の表示が縮小され、今回のプログラムでは作りやすい設定になります。
それでは背景を配置してみましょう(図2-4)。
アレンジャーの使い方3
図2-4: アレンジャーの使い方3
(1)の「Img」を選択し(2)を適当にドラッグして配置できたら、配置したオブジェクトが選択されている状態で(3)の「位置」を「0×0-1600×900」に設定します。 すると、オブジェクトが真っ黒な画面にぴったり合うサイズになります。 オブジェクトを選択するには、(2)で直接オブジェクトをクリックするか、(4)から選択します。
次に、ここまでをいったん適当なファイルに保存してから「Ctrl+Shift+R」を押します。 表示された「res」フォルダに、前回と同様「samples/free_resources/dot_back_side.png」をコピーしてきて配置します。
配置した画像をImgオブジェクトに設定しましょう。 (4)から「img1」を選択し、(3)の「プロパティの編集...」を押して出てくるダイアログで、表2-1の通りに設定し、「OK」ボタンを押します。 画像が表示されれば成功です。
表2-1: img1のプロパティ
プロパティ 入力する値
color 0xFFAAAAAA
scrHeight 450
scrWidth 800
texPath res/dot_back_side.png
同様の流れでメニュー画面を作りましょう(図2-5)。
アレンジャーの使い方4
図2-5: アレンジャーの使い方4
「Text」オブジェクトを4つ、「Rect」オブジェクトを2つ、「Circle」オブジェクトを1つ配置し、それぞれ位置を表2-2のように設定します。
表2-2: 各オブジェクトの位置
オブジェクト名 位置
text1 150 × 100 - 200 × 100
text2 150 × 200 - 200 × 100
text3 150 × 300 - 200 × 100
text4 150 × 400 - 200 × 100
rect1 400 × 100 - 1000 × 50
rect2 405 × 105 - 990 × 40
circle1 70 × 100 - 50 × 50
また各オブジェクトに対し、選んで下のほうにある「プロパティの編集...」を押して、表2-3のように設定します。
表2-3: 各オブジェクトのプロパティ
オブジェクト名 プロパティ 入力する値
text1 text 音量
text2 text セーブ
text3 text ロード
text4 text 終了
rect1 colorFill 0x00000000
colorStroke 0xFFFFFFFF
strokeWidth 2.0
rect2 colorFill 0xFFAAFF80
circle1 colorFill 0x99FF9988
colorStroke 0xFFFFFFFF
strokeWidth 3.0
メニュー画面の配置が完成しました。 試しに、自動生成されたプログラムを確認してみましょう。 Kuinエディタ上部の「コードを表示」ボタンをクリックするとプログラムが表示されます(図2-6)。
アレンジャーの使い方5
図2-6: アレンジャーの使い方5
(1)を押すと自動生成されたプログラムが表示され、もう一度押すと配置画面に戻ります。 自動生成されたプログラムを見ると、「init」「fin」「draw」という関数が生成されていることが分かります。
これらをメインのプログラムから呼び出せば、配置した画面が描画できます。 (2)から「\main」を選んで、メインのプログラムを表示して図2-7のプログラムを追加します。
  1. enum Sequence
  2.   fadeIn
  3.   menu
  4.   fadeOut
  5. end enum
  6.  
  7. var sequence: @Sequence
  8.  
  9. var wndMain: wnd@Wnd
  10. var drawMain: wnd@Draw
  11.  
  12. func main()
  13.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  14.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  15.  
  16.   do @sequence :: %fadeIn
  17.   do wipe@set(%fade, true, 60, draw@black)
  18.   do draw@clearColor(draw@white)
  19.  
  20.   do \menu@init()
  21.  
  22.   while(wnd@act())
  23.     do \menu@draw()
  24.     switch(@sequence)
  25.     case %fadeIn
  26.       if(wipe@draw())
  27.         do @sequence :: %menu
  28.       end if
  29.     case %menu
  30.       do @sequence :: %fadeOut
  31.       do wipe@set(%fade, false, 60, draw@black)
  32.     case %fadeOut
  33.       if(wipe@draw())
  34.         ret
  35.       end if
  36.     end switch
  37.  
  38.     do draw@render(60)
  39.   end while
  40.  
  41.   do \menu@fin()
  42. end func
図2-7: menu3.kn
実行すると、白で表示していた背景の代わりに、作成したメニュー画面が表示されます(図2-8)。
メニュー画面の描画
図2-8: メニュー画面の描画

2.2カーソル移動



このままではカーソルが移動しないため、カーソル移動の処理を追加します。
自前で実装しても良いですが、カーソル移動の処理は多くの場面で使うため、既に「cursor」ライブラリに用意されています。 今回はそれを利用します(図2-9)。
  1. enum Sequence
  2.   fadeIn
  3.   menu
  4.   fadeOut
  5. end enum
  6.  
  7. var sequence: @Sequence
  8.  
  9. var wndMain: wnd@Wnd
  10. var drawMain: wnd@Draw
  11.  
  12. func main()
  13.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  14.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  15.  
  16.   do @sequence :: %fadeIn
  17.   do wipe@set(%fade, true, 60, draw@black)
  18.   do draw@clearColor(draw@white)
  19.  
  20.   do \menu@init()
  21.  
  22.   var cursor: cursor@Cursor :: #cursor@Cursor
  23.   do cursor.set(4, 0, false, true)
  24.  
  25.   while(wnd@act())
  26.     do \menu@draw()
  27.     switch(@sequence)
  28.     case %fadeIn
  29.       if(wipe@draw())
  30.         do @sequence :: %menu
  31.       end if
  32.     case %menu
  33.       do cursor.update()
  34.       do \menu@circle1.y :: (cursor.get() + 1) $ float * 100.0
  35.       if(input@pad(0, %a) = 1)
  36.         switch(cursor.get())
  37.         case 3
  38.           do @sequence :: %fadeOut
  39.           do wipe@set(%fade, false, 60, draw@black)
  40.         end switch
  41.       end if
  42.     case %fadeOut
  43.       if(wipe@draw())
  44.         ret
  45.       end if
  46.     end switch
  47.  
  48.     do draw@render(60)
  49.   end while
  50.  
  51.   do \menu@fin()
  52. end func
図2-9: menu4.kn
22行目で「cursor@Cursor」クラスのインスタンスを作成し、23行目で初期化しています。 setメソッドの括弧内は順に「カーソルが選択する項目の数」「カーソルの初期値」「上下キーで移動させるならfalse、左右キーで移動させるならtrue」「最初と最後の項目が行き来できるか」です。
33行目のように「update」メソッドを呼ぶことで、キー入力によるカーソル移動が自動で処理されます。 34行目のように「get」メソッドを呼ぶと、カーソルの位置が0から始まる整数で返ります。 ここではそれに応じて、円のY座標を移動させています。
35行目から41行目は、Zキー(Aボタン)が押されたときに、カーソルが3番目の位置なら終了するようにしています。 3番目は「終了」の位置です。
実行すると、上下のキーでカーソルが移動でき、「終了」を選んでZキーを押すと終了することが確認できます。

2.3音量ゲージの増減と決定処理



次は、音量ゲージの増減と決定処理を追加します。 ついでに、音量を確認するためのBGMを鳴らしましょう。 「Ctrl+Shift+R」を押して、「res」フォルダに「samples/free_resources/bgm_sample.ogg」をコピーしてきて配置します。
配置できたら、図2-10のプログラムを追加します。
  1. enum Sequence
  2.   fadeIn
  3.   menu
  4.   fadeOut
  5. end enum
  6.  
  7. var sequence: @Sequence
  8.  
  9. var wndMain: wnd@Wnd
  10. var drawMain: wnd@Draw
  11.  
  12. func main()
  13.   do ogg@init()
  14.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  15.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  16.  
  17.   do @sequence :: %fadeIn
  18.   do wipe@set(%fade, true, 60, draw@black)
  19.   do draw@clearColor(draw@white)
  20.  
  21.   do \menu@init()
  22.  
  23.   var cursor: cursor@Cursor :: #cursor@Cursor
  24.   do cursor.set(4, 0, false, true)
  25.   var volume: cursor@Cursor :: #cursor@Cursor
  26.   do volume.set(11, 10, true, false)
  27.   do bgm@play("res/bgm_sample.ogg", 0.0)
  28.  
  29.   while(wnd@act())
  30.     do \menu@draw()
  31.     switch(@sequence)
  32.     case %fadeIn
  33.       if(wipe@draw())
  34.         do @sequence :: %menu
  35.       end if
  36.     case %menu
  37.       do cursor.update()
  38.       do \menu@circle1.y :: (cursor.get() + 1) $ float * 100.0
  39.       do volume.update()
  40.       do \menu@rect2.width :: volume.get() $ float * 99.0
  41.       do bgm@volume(volume.get() $ float / 10.0)
  42.       if(input@pad(0, %a) = 1)
  43.         switch(cursor.get())
  44.         case 3
  45.           do @sequence :: %fadeOut
  46.           do wipe@set(%fade, false, 60, draw@black)
  47.         end switch
  48.       end if
  49.     case %fadeOut
  50.       if(wipe@draw())
  51.         ret
  52.       end if
  53.     end switch
  54.  
  55.     do draw@render(60)
  56.   end while
  57.  
  58.   do \menu@fin()
  59. end func
図2-10: menu5.kn
25行目から26行目、40行目から41行目は先ほどと同様のカーソルの処理です。 左右キーで0以上10以下の値が増減できるようにしています。
27行目の「bgm@play」で、BGMを再生しています。 bgmライブラリとは、sndライブラリをラップした、BGM再生を便利にできるものです。 同時に1つの音楽ファイルしか扱えませんが、インスタンスを作らずに鳴らせ、またクロスフェード再生などもできます。
bgm@playの括弧内は順に「ファイル名」「再生開始位置(秒)」です。 ogg形式の音声ファイルを再生するには、13行目のように「ogg@init」を一度呼んでおく必要があります。
39行目から40行目で、左右のキーに応じて音量ゲージの長さを調節し、またその値に応じて41行目で実際にBGMの音量を変更しています。
実行して「音量」を選んだ状態で左右のキーを押すと、音量ゲージとBGMの大きさが増減することが確認できます(図2-11)。
メニュー画面の操作
図2-11: メニュー画面の操作

3セーブとロード

3.1セーブとロード



最後に、音量の値をセーブする機能を追加します(図3-1)。
  1. enum Sequence
  2.   fadeIn
  3.   menu
  4.   fadeOut
  5. end enum
  6.  
  7. var sequence: @Sequence
  8.  
  9. var wndMain: wnd@Wnd
  10. var drawMain: wnd@Draw
  11.  
  12. class SaveData()
  13.   +var volume: int
  14. end class
  15.  
  16. func main()
  17.   do ogg@init()
  18.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  19.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  20.  
  21.   do @sequence :: %fadeIn
  22.   do wipe@set(%fade, true, 60, draw@black)
  23.   do draw@clearColor(draw@white)
  24.  
  25.   do \menu@init()
  26.  
  27.   var cursor: cursor@Cursor :: #cursor@Cursor
  28.   do cursor.set(4, 0, false, true)
  29.   var volume: cursor@Cursor :: #cursor@Cursor
  30.   do volume.set(11, 10, true, false)
  31.   do bgm@play("res/bgm_sample.ogg", 0.0)
  32.  
  33.   var key: []bit8 :: [
  34.   |0x01b8, 0x23b8, 0x45b8, 0x67b8, 0x89b8, 0xABb8, 0xCDb8, 0xEFb8,
  35.   |0x01b8, 0x23b8, 0x45b8, 0x67b8, 0x89b8, 0xABb8, 0xCDb8, 0xEFb8,
  36.   |0x01b8, 0x23b8, 0x45b8, 0x67b8, 0x89b8, 0xABb8, 0xCDb8, 0xEFb8,
  37.   |0x01b8, 0x23b8, 0x45b8, 0x67b8, 0x89b8, 0xABb8, 0xCDb8, 0xEFb8]
  38.   var path: []char :: wnd@sysDir(%appData) ~ "TestKuin/Test.bin"
  39.  
  40.   while(wnd@act())
  41.     do \menu@draw()
  42.     switch(@sequence)
  43.     case %fadeIn
  44.       if(wipe@draw())
  45.         do @sequence :: %menu
  46.       end if
  47.     case %menu
  48.       do cursor.update()
  49.       do \menu@circle1.y :: (cursor.get() + 1) $ float * 100.0
  50.       do volume.update()
  51.       do \menu@rect2.width :: volume.get() $ float * 99.0
  52.       do bgm@volume(volume.get() $ float / 10.0)
  53.       if(input@pad(0, %a) = 1)
  54.         switch(cursor.get())
  55.         case 1
  56.           var saveData: @SaveData :: #@SaveData
  57.           do saveData.volume :: volume.get()
  58.           var bins: []bit8 :: cipher@encrypt(saveData $> []bit8, key)
  59.           do file@makeDir(file@dir(path))
  60.           var file: file@Writer :: file@makeWriter(path, false)
  61.           if(file <>& null)
  62.             do file.write(bins)
  63.             do file.fin()
  64.           end if
  65.         case 2
  66.           var file: file@Reader :: file@makeReader(path)
  67.           if(file <>& null)
  68.             var bins: []bit8 :: cipher@decrypt(file.read(file.fileSize()), key)
  69.             do file.fin()
  70.             var saveData: @SaveData :: bins $< @SaveData
  71.             do volume.set(11, saveData.volume, true, false)
  72.           end if
  73.         case 3
  74.           do @sequence :: %fadeOut
  75.           do wipe@set(%fade, false, 60, draw@black)
  76.         end switch
  77.       end if
  78.     case %fadeOut
  79.       if(wipe@draw())
  80.         ret
  81.       end if
  82.     end switch
  83.  
  84.     do draw@render(60)
  85.   end while
  86.  
  87.   do \menu@fin()
  88. end func
図3-1: menu6.kn
今回保存するのは音量のみですが、実際のゲームではライフやお金などの様々な値を保存することになります。 そこで、12行目から14行目のようにセーブデータ用に値をまとめたクラスを用意しておきます。
33行目から38行目では、暗号化に使う鍵と、セーブデータのファイルパスを定義しています。 「0xb8」は、16進数のbit8型の値を意味し、bit8型とは8ビットの値を扱う型です。 鍵はパスワードのようなものなので、実際にゲームを作るときには自由に設定して誰にも知られないようにすべきです。
「セーブ」が押されると、55行目から64行目で、作成したクラスに音量の値を入れて暗号化して保存します。 暗号化には「cipher@encrypt」関数を使い、括弧内は順に「暗号化するデータ」「鍵」です。 データは[]bit8型で指定する必要があり、「$>[]bit8」と書くと、クラスを丸ごと[]bit8型に変換できます。 暗号化したデータは「file」ライブラリを使って保存します。
保存したデータのロードも同様です。 65行目から72行目で、fileライブラリでファイルをロードし、データの復号には「cipher@decrypt」関数を使います。 複合したデータは[]bit8型なため、71行目の「$<データの型」と書くことで元のクラスの型に変換します。 fileライブラリの詳しい使い方は「テキスト、バイナリファイルを読み書きする」をご覧ください。
実行し、「音量」を変更して「セーブ」でZキーを押してから終了し、もう一度起動して「ロード」でZキーを押すと、先ほどの音量の値が復元されます。
以上でメニュー画面の完成です。 次回は、電卓のアプリを作ります。
1713489565jaf