2025年02月19日くいなちゃん
「プログラミング言語Kuin」の基本機能を学ぶ、実行環境がexe用のチュートリアル5です。 今回は、地味ながらもゲーム制作では重要なメニュー画面とセーブ機能を作ってみます。
1シーケンスの設計
1.1シーケンスの作成
ゲームは普通、本編のほかにも、タイトル画面やメニュー画面などいくつものシーンで構成されています。 これらのシーンがどういう順番でどう切り替わるかを「シーケンス」といいます。 プログラムを書く前に、まずはシーケンスを考えてみましょう。
今回作るメニュー画面は、図1-1のようなシーケンスで作ることにしました。

まずは大まかな流れになっている「フェードイン、カーソル選択、フェードアウト」というシーケンスをプログラムで書いてみます。 前回と同様、スニペットから「mainとdrawコントロール」を選んでから、図1-2のコードを追加します。
- enum Sequence
- fadeIn
- menu
- fadeOut
- end enum
-
- var sequence: @Sequence
-
- var wndMain: wnd@Wnd
- var drawMain: wnd@Draw
-
- func main()
- do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
- do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
-
- do @sequence :: %fadeIn
-
- while(wnd@act())
- switch(@sequence)
- case %fadeIn
- case %menu
- case %fadeOut
- end switch
-
- do draw@render(60)
- end while
- end func
1行目から7行目でシーケンスの列挙型と変数を作成し、16行目で初期化し、メインループ中の19行目から23行目ではswitchブロックで、シーケンスに応じた処理をするために分岐しています。
シーケンスだけで中身が無いため、実行しても何も起こりませんが、とりあえず大まかな流れはできました。
1.2フェードイン、フェードアウト
「フェードイン」と「フェードアウト」の中身を実装します。
自前でフェードの効果を実装しても良いですが、既に「wipe」ライブラリにフェードを簡単に行う仕組みが用意されているため、それを利用します(図1-3)。
- enum Sequence
- fadeIn
- menu
- fadeOut
- end enum
-
- var sequence: @Sequence
-
- var wndMain: wnd@Wnd
- var drawMain: wnd@Draw
-
- func main()
- do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
- do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
-
- do @sequence :: %fadeIn
- do wipe@set(%fade, true, 60, draw@black)
- do draw@clearColor(draw@white)
-
- while(wnd@act())
- switch(@sequence)
- case %fadeIn
- if(wipe@draw())
- do @sequence :: %menu
- end if
- case %menu
- do @sequence :: %fadeOut
- do wipe@set(%fade, false, 60, draw@black)
- case %fadeOut
- if(wipe@draw())
- ret
- end if
- end switch
-
- do draw@render(60)
- end while
- end func
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)。

2メニュー画面の作成
2.1アレンジャー
さて、ここからはメニュー画面を作成しますが、今回はKuinの視覚的に配置できる機能を使ってみます。 図2-1の流れになります。

メニュー画面を視覚的に作成すると、Kuinのプログラムが自動生成されるという流れです。
この機能を使うには、Kuinエディタの右下のほうにある「追加」をクリックし、新しい「.kn」ファイルを追加します(図2-2)。

図の(1)を押すと出てくるダイアログから、(2)の「2D描画」を選択し、(3)で「menu」とファイル名を入力し、(4)の「追加」のボタンを押すと「.kn」ファイルが追加されます。
追加した画面が図2-3です。

(1)で「.kn」ファイルが切り替えられます。 (2)で画面の設定ができます。
今回のプログラムは画面のサイズが1600×900なため、(2)の「位置」を「0×0-1600×900」に設定しておきます。 また(2)の「ズーム」の「-」ボタンを押して「0.5」にしておくと、Kuinエディタ上の表示が縮小され、今回のプログラムでは作りやすい設定になります。
それでは背景を配置してみましょう(図2-4)。

(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」ボタンを押します。 画像が表示されれば成功です。
プロパティ | 入力する値 |
---|---|
color | 0xFFAAAAAA |
scrHeight | 450 |
scrWidth | 800 |
texPath | res/dot_back_side.png |
同様の流れでメニュー画面を作りましょう(図2-5)。

「Text」オブジェクトを4つ、「Rect」オブジェクトを2つ、「Circle」オブジェクトを1つ配置し、それぞれ位置を表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のように設定します。
オブジェクト名 | プロパティ | 入力する値 |
---|---|---|
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)。

(1)を押すと自動生成されたプログラムが表示され、もう一度押すと配置画面に戻ります。 自動生成されたプログラムを見ると、「init」「fin」「draw」という関数が生成されていることが分かります。
これらをメインのプログラムから呼び出せば、配置した画面が描画できます。 (2)から「\main」を選んで、メインのプログラムを表示して図2-7のプログラムを追加します。
- enum Sequence
- fadeIn
- menu
- fadeOut
- end enum
-
- var sequence: @Sequence
-
- var wndMain: wnd@Wnd
- var drawMain: wnd@Draw
-
- func main()
- do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
- do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
-
- do @sequence :: %fadeIn
- do wipe@set(%fade, true, 60, draw@black)
- do draw@clearColor(draw@white)
-
- do \menu@init()
-
- while(wnd@act())
- do \menu@draw()
- switch(@sequence)
- case %fadeIn
- if(wipe@draw())
- do @sequence :: %menu
- end if
- case %menu
- do @sequence :: %fadeOut
- do wipe@set(%fade, false, 60, draw@black)
- case %fadeOut
- if(wipe@draw())
- ret
- end if
- end switch
-
- do draw@render(60)
- end while
-
- do \menu@fin()
- end func
実行すると、白で表示していた背景の代わりに、作成したメニュー画面が表示されます(図2-8)。

2.2カーソル移動
このままではカーソルが移動しないため、カーソル移動の処理を追加します。
自前で実装しても良いですが、カーソル移動の処理は多くの場面で使うため、既に「cursor」ライブラリに用意されています。 今回はそれを利用します(図2-9)。
- enum Sequence
- fadeIn
- menu
- fadeOut
- end enum
-
- var sequence: @Sequence
-
- var wndMain: wnd@Wnd
- var drawMain: wnd@Draw
-
- func main()
- do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
- do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
-
- do @sequence :: %fadeIn
- do wipe@set(%fade, true, 60, draw@black)
- do draw@clearColor(draw@white)
-
- do \menu@init()
-
- var cursor: cursor@Cursor :: #cursor@Cursor
- do cursor.set(4, 0, false, true)
-
- while(wnd@act())
- do \menu@draw()
- switch(@sequence)
- case %fadeIn
- if(wipe@draw())
- do @sequence :: %menu
- end if
- case %menu
- do cursor.update()
- do \menu@circle1.y :: (cursor.get() + 1) $ float * 100.0
- if(input@pad(0, %a) = 1)
- switch(cursor.get())
- case 3
- do @sequence :: %fadeOut
- do wipe@set(%fade, false, 60, draw@black)
- end switch
- end if
- case %fadeOut
- if(wipe@draw())
- ret
- end if
- end switch
-
- do draw@render(60)
- end while
-
- do \menu@fin()
- end func
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のプログラムを追加します。
- enum Sequence
- fadeIn
- menu
- fadeOut
- end enum
-
- var sequence: @Sequence
-
- var wndMain: wnd@Wnd
- var drawMain: wnd@Draw
-
- func main()
- do ogg@init()
- do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
- do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
-
- do @sequence :: %fadeIn
- do wipe@set(%fade, true, 60, draw@black)
- do draw@clearColor(draw@white)
-
- do \menu@init()
-
- var cursor: cursor@Cursor :: #cursor@Cursor
- do cursor.set(4, 0, false, true)
- var volume: cursor@Cursor :: #cursor@Cursor
- do volume.set(11, 10, true, false)
- do bgm@play("res/bgm_sample.ogg", 0.0)
-
- while(wnd@act())
- do \menu@draw()
- switch(@sequence)
- case %fadeIn
- if(wipe@draw())
- do @sequence :: %menu
- end if
- case %menu
- do cursor.update()
- do \menu@circle1.y :: (cursor.get() + 1) $ float * 100.0
- do volume.update()
- do \menu@rect2.width :: volume.get() $ float * 99.0
- do bgm@volume(volume.get() $ float / 10.0)
- if(input@pad(0, %a) = 1)
- switch(cursor.get())
- case 3
- do @sequence :: %fadeOut
- do wipe@set(%fade, false, 60, draw@black)
- end switch
- end if
- case %fadeOut
- if(wipe@draw())
- ret
- end if
- end switch
-
- do draw@render(60)
- end while
-
- do \menu@fin()
- end func
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)。

3セーブとロード
3.1セーブとロード
最後に、音量の値をセーブする機能を追加します(図3-1)。
- enum Sequence
- fadeIn
- menu
- fadeOut
- end enum
-
- var sequence: @Sequence
-
- var wndMain: wnd@Wnd
- var drawMain: wnd@Draw
-
- class SaveData()
- +var volume: int
- end class
-
- func main()
- do ogg@init()
- do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
- do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
-
- do @sequence :: %fadeIn
- do wipe@set(%fade, true, 60, draw@black)
- do draw@clearColor(draw@white)
-
- do \menu@init()
-
- var cursor: cursor@Cursor :: #cursor@Cursor
- do cursor.set(4, 0, false, true)
- var volume: cursor@Cursor :: #cursor@Cursor
- do volume.set(11, 10, true, false)
- do bgm@play("res/bgm_sample.ogg", 0.0)
-
- var key: []bit8 :: [
- |0x01b8, 0x23b8, 0x45b8, 0x67b8, 0x89b8, 0xABb8, 0xCDb8, 0xEFb8,
- |0x01b8, 0x23b8, 0x45b8, 0x67b8, 0x89b8, 0xABb8, 0xCDb8, 0xEFb8,
- |0x01b8, 0x23b8, 0x45b8, 0x67b8, 0x89b8, 0xABb8, 0xCDb8, 0xEFb8,
- |0x01b8, 0x23b8, 0x45b8, 0x67b8, 0x89b8, 0xABb8, 0xCDb8, 0xEFb8]
- var path: []char :: wnd@sysDir(%appData) ~ "TestKuin/Test.bin"
-
- while(wnd@act())
- do \menu@draw()
- switch(@sequence)
- case %fadeIn
- if(wipe@draw())
- do @sequence :: %menu
- end if
- case %menu
- do cursor.update()
- do \menu@circle1.y :: (cursor.get() + 1) $ float * 100.0
- do volume.update()
- do \menu@rect2.width :: volume.get() $ float * 99.0
- do bgm@volume(volume.get() $ float / 10.0)
- if(input@pad(0, %a) = 1)
- switch(cursor.get())
- case 1
- var saveData: @SaveData :: #@SaveData
- do saveData.volume :: volume.get()
- var bins: []bit8 :: cipher@encrypt(saveData $> []bit8, key)
- do file@makeDir(file@dir(path))
- var file: file@Writer :: file@makeWriter(path, false)
- if(file <>& null)
- do file.write(bins)
- do file.fin()
- end if
- case 2
- var file: file@Reader :: file@makeReader(path)
- if(file <>& null)
- var bins: []bit8 :: cipher@decrypt(file.read(file.fileSize()), key)
- do file.fin()
- var saveData: @SaveData :: bins $< @SaveData
- do volume.set(11, saveData.volume, true, false)
- end if
- case 3
- do @sequence :: %fadeOut
- do wipe@set(%fade, false, 60, draw@black)
- end switch
- end if
- case %fadeOut
- if(wipe@draw())
- ret
- end if
- end switch
-
- do draw@render(60)
- end while
-
- do \menu@fin()
- end func
今回保存するのは音量のみですが、実際のゲームではライフやお金などの様々な値を保存することになります。 そこで、12行目から14行目のようにセーブデータ用に値をまとめたクラスを用意しておきます。
33行目から38行目では、暗号化に使う鍵と、セーブデータのファイルパスを定義しています。 「0x数b8」は、16進数のbit8型の値を意味し、bit8型とは8ビットの値を扱う型です。 鍵はパスワードのようなものなので、実際にゲームを作るときには自由に設定して誰にも知られないようにすべきです。
「セーブ」が押されると、55行目から64行目で、作成したクラスに音量の値を入れて暗号化して保存します。 暗号化には「cipher@encrypt」関数を使い、括弧内は順に「暗号化するデータ」「鍵」です。 データは[]bit8型で指定する必要があり、「$>[]bit8」と書くと、クラスを丸ごと[]bit8型に変換できます。 暗号化したデータは「file」ライブラリを使って保存します。
保存したデータのロードも同様です。 65行目から72行目で、fileライブラリでファイルをロードし、データの復号には「cipher@decrypt」関数を使います。 複合したデータは[]bit8型なため、71行目の「$<データの型」と書くことで元のクラスの型に変換します。 fileライブラリの詳しい使い方は「テキスト、バイナリファイルを読み書きする」をご覧ください。
実行し、「音量」を変更して「セーブ」でZキーを押してから終了し、もう一度起動して「ロード」でZキーを押すと、先ほどの音量の値が復元されます。
以上でメニュー画面の完成です。 次回は、電卓のアプリを作ります。