2024年12月22日くいなちゃん


プログラミング言語Kuin」の基本機能を学ぶ、実行環境がexe用のチュートリアル4です。 今回は横視点のアクションゲームを作ってみます。

1画像の用意

1.1ウインドウの作成



今回は画像を使ったゲームを作ります。 完成画面は図1-1の通りです。
完成画面
図1-1: 完成画面
まずは前回と同様、スニペットから「mainとdrawコントロール」のコードを挿入してください(図1-2)。
  1. var wndMain: wnd@Wnd
  2. var drawMain: wnd@Draw
  3.  
  4. func main()
  5.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  6.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  7.  
  8.   while(wnd@act())
  9.     do draw@render(60)
  10.   end while
  11. end func
図1-2: kui_action1.kn
この状態で、ひとまず適当なフォルダにソースコードを保存しましょう。 空のフォルダを作成し、その中に「main.kn」など、好きな名前で保存してください。
Kuinのソースコード名には、小文字アルファベット、数字、「_」の記号が使え、拡張子は「.kn」です。 ただし先頭の文字には数字が使えません。

1.2画像の用意



それではゲームで使う画像を用意します。
まずは図1-3の「resフォルダを開く」をクリックし、resフォルダを開いてください。 先ほど保存した.knファイルの場所に「res」という名前のフォルダが作られ、それが開かれたと思います。
「res」フォルダを開く
図1-3: 「res」フォルダを開く
Kuinでは、プログラム上で読み込むファイルは基本的にこの「res」フォルダの中に入れます。 ここに入れておくと、リリースビルド時に「res」フォルダは暗号化されて1つのファイルにまとまります。
さて、今回読み込ませる画像は自分で描いてもいいですが、今回はKuinに付属しているフリー素材を使いましょう。 「samples/free_resources/」に入っているファイルのうち、「dot_back_side.png」「dot_kuina_chan.png」「dot_map_chips_side.png」「map_sample_side.txt」の4つを「res」フォルダの中にコピーしてください(図1-4)。
画像の用意
図1-4: 画像の用意
これで画像をプログラムから呼び出せるようになりました。

2背景の表示

まずは背景画像を読み込んで表示してみましょう。 図2-1のプログラムを追加します。
  1. var wndMain: wnd@Wnd
  2. var drawMain: wnd@Draw
  3. var texBack: draw@Tex
  4.  
  5. func main()
  6.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  7.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  8.   do @texBack :: draw@makeTex("res/dot_back_side.png")
  9.   do draw@sampler(%point)
  10.  
  11.   while(wnd@act())
  12.     do @texBack.drawScale(0.0, 0.0, 1600.0, 900.0, 0.0, 0.0, 800.0, 450.0, draw@white)
  13.     do draw@render(60)
  14.   end while
  15. end func
図2-1: kui_action2.kn
3行目の「draw@Tex」は、画像を格納できるクラスです。 8行目の「draw@makeTex」関数で、先ほど配置した「res/dot_back_side.png」の画像ファイルを読み込み、変数「@texBack」に代入しています。 「draw@makeTex」関数の括弧内は、読み込む画像のファイルパスです。
12行目の「@texBack.drawScale」関数で、「@texBack」に格納されている画像を描画しています。 この「.drawScale」は「draw@Tex」クラスの中にある関数です。 「.drawScale」は画像を引き伸ばして描画する関数で、括弧内はそれぞれ「転送先の座標X」「座標Y」「幅」「高さ」「転送元の座標X」「座標Y」「幅」「高さ」「色」です(図2-2)。
drawScale関数
図2-2: drawScale関数
色として指定している「draw@white」は白色を意味し、「0xFFFFFFFF」と書くことと同じです。 白色を指定すると画像はそのまま描画されますが、黒色に近づけると画像は暗く描画されます。
9行目の「draw@sampler」関数は、画像を引き伸ばしたときの補間方法を設定する関数で、「%point」を指定すると補間されず、「%linear」を指定すると補間されて見た目がなめらかになります。 今回はドットがはっきりした見た目にしたいため、補間なしの%pointにしています。
実行すると図2-3のようになります。
背景の描画
図2-3: 背景の描画

3キャラクターの表示

それではキャラクターの表示に着手しますが、その前にキャラクターの座標を扱うクラスを作成しましょう。
今回はマップと衝突したときに物理挙動を実現させたいため、Kuinに用意されている便利なクラス「game@Rect」を継承して利用します。 「継承」とは、既存のクラスに変数や関数を追加して新しいクラスを作成することです(図3-1)。
  1. class Player(game@Rect)
  2.   +var toRight: bool
  3. end class
  4.  
  5. var wndMain: wnd@Wnd
  6. var drawMain: wnd@Draw
  7. var texBack: draw@Tex
  8. var texPlayer: draw@Tex
  9. var player: @Player
  10.  
  11. func main()
  12.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  13.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  14.   do @texBack :: draw@makeTex("res/dot_back_side.png")
  15.   do @texPlayer :: draw@makeTex("res/dot_kuina_chan.png")
  16.   do @player :: #@Player
  17.   do @player.x :: 96.0
  18.   do @player.y :: 800.0
  19.   do @player.width :: 46.0
  20.   do @player.height :: 116.0
  21.   do @player.toRight :: true
  22.   do draw@sampler(%point)
  23.  
  24.   while(wnd@act())
  25.     do @texBack.drawScale(0.0, 0.0, 1600.0, 900.0, 0.0, 0.0, 800.0, 450.0, draw@white)
  26.     do @texPlayer.drawScale(@player.x - 64.0, @player.y - 64.0, 128.0, 128.0, 0.0, 0.0, 64.0, 64.0, draw@white)
  27.     do draw@render(60)
  28.   end while
  29. end func
図3-1: kui_action3.kn
1~3行目で@Playerクラスを作成しています。 括弧内に「game@Rect」と書いていますが、このように括弧内に既存のクラス名を書くことで、そのクラスの中身を引き継いでクラスを作ることができます。 game@Rectクラスは、「x」「y」などの座標を入れる変数を持っています。
2行目でクラスに追加している「toRight」変数の型は「bool」になっていますが、bool型とは真偽を格納する型です。 プログラム上では、真は「true」と書き、偽は「false」と書きます。 「toRight」変数にはキャラクターが右を向いているかどうかを格納する予定で、右を向いているなら「true」、左を向いているなら「false」を入れます。
8~9行目で、プレイヤー用の画像を入れる変数「@texPlayer」と、先ほど作成した@Playerクラス型の変数「@player」を作り、15~21行目でそれぞれの中身を設定しています。
26行目で、プレイヤーの座標「@player.x」「@player.y」の位置にキャラクターを描画しています。 プレイヤーの座標がちょうど画像の中心になるように描画したいので、キャラクターの幅と高さの半分である64.0を引き算しています(図3-2)。
画像の中心
図3-2: 画像の中心
実行すると図3-3のようになります。
プレイヤーの描画
図3-3: プレイヤーの描画

4キャラクターの移動

キャラクターの移動処理を追加します(図4-1)。
  1. class Player(game@Rect)
  2.   +var toRight: bool
  3. end class
  4.  
  5. var wndMain: wnd@Wnd
  6. var drawMain: wnd@Draw
  7. var texBack: draw@Tex
  8. var texPlayer: draw@Tex
  9. var player: @Player
  10.  
  11. func main()
  12.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  13.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  14.   do @texBack :: draw@makeTex("res/dot_back_side.png")
  15.   do @texPlayer :: draw@makeTex("res/dot_kuina_chan.png")
  16.   do @player :: #@Player
  17.   do @player.x :: 96.0
  18.   do @player.y :: 800.0
  19.   do @player.width :: 46.0
  20.   do @player.height :: 116.0
  21.   do @player.toRight :: true
  22.   do draw@sampler(%point)
  23.   var pattern: []float :: [0.0, 64.0, 0.0, 128.0]
  24.  
  25.   while(wnd@act())
  26.     do @texBack.drawScale(0.0, 0.0, 1600.0, 900.0, 0.0, 0.0, 800.0, 450.0, draw@white)
  27.     if(input@pad(0, %left) >= 1)
  28.       do @player.veloX :- 0.8
  29.       do @player.toRight :: false
  30.     end if
  31.     if(input@pad(0, %right) >= 1)
  32.       do @player.veloX :+ 0.8
  33.       do @player.toRight :: true
  34.     end if
  35.     do @player.move(0.0)
  36.     do @texPlayer.drawScale(@player.x - 64.0, @player.y - 64.0, 128.0, 128.0, pattern[draw@cnt() / 12 % 4], @player.toRight ?(128.0, 0.0), 64.0, 64.0, draw@white)
  37.     do draw@render(60)
  38.   end while
  39. end func
図4-1: kui_action4.kn
27行目~34行目で、右や左キーが押されたときに、プレイヤーの速度を表す「@player.veloX」変数を増減させ、プレイヤーが向いている方向も設定しています。
35行目の「@player.move(0.0)」は、プレイヤーの位置に速度を加算して移動させる関数です。 括弧内に0.0より大きい値を入れると、速度がその値を超えないように自動調整されます。 ただし今回は最大速度を縦と横とで別々に設定したいため、自動調整されないように0.0を渡しています。
36行目の書き換えにより、キャラクターをアニメーションさせています。 「@player.toRight ?(128.0, 0.0)」の部分は、プレイヤーが右向きなら128.0、左向きなら0.0になります。 「?(,)」は、if文のように条件分岐する演算子で、「A ?(B, C)」と書くと、AがtrueならB、AがfalseならCを返します。
36行目の「pattern[~]」の部分は、23行目で作成した「配列」の変数にアクセスしています。 「配列」とは変数を複数個並べたようなもので、例えば23行目のように変数の型を「[]float」とすると、floatの値を複数個並べたものが格納できます。 配列の値を読み書きするときは、「変数名[要素番号]」と書きます。
23行目のpattern変数には「[0.0, 64.0, 0.0, 128.0]」を代入していますが、このときpattern[0]と書くと0.0、pattern[1]と書くと64.0、pattern[2]と書くと0.0、pattern[3]と書くと128.0の値が取得できます。
23行目の変数は、他の変数とは違って関数の中で作成していますが、こうすると関数が呼び出されたときに作成されて、関数を抜けるときに破棄される変数になります。 このような関数内の変数を「ローカル変数」といい、関数外の変数を「グローバル変数」といいます。 グローバル変数にアクセスするときには変数名の先頭に「@」を付ける必要があります。
そして36行目の「draw@cnt()」は、プログラムの起動から何フレーム経ったかを返す関数です。 割り算の「/」演算子を整数型に対して行うと小数点以下が切り捨てられることと、割り算の余りを求める「%」演算子を組み合わせると、「draw@cnt() / 12 % 4」で「0、1、2、3、0、1、2、3、0、…」と繰り返すパターンが生み出されます。 これにより歩行アニメーションを描画しています。
実行すると図4-2のようになります。
プレイヤーの移動
図4-2: プレイヤーの移動

5マップの描画

次はマップの描画です。 図5-1のプログラムを追加します。
  1. class Player(game@Rect)
  2.   +var toRight: bool
  3. end class
  4.  
  5. var wndMain: wnd@Wnd
  6. var drawMain: wnd@Draw
  7. var texBack: draw@Tex
  8. var texPlayer: draw@Tex
  9. var player: @Player
  10. var texMap: draw@Tex
  11. var map: game@Map
  12.  
  13. func main()
  14.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  15.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  16.   do @texBack :: draw@makeTex("res/dot_back_side.png")
  17.   do @texPlayer :: draw@makeTex("res/dot_kuina_chan.png")
  18.   do @texMap :: draw@makeTex("res/dot_map_chips_side.png")
  19.   do @map :: game@makeMap("res/map_sample_side.txt", 64.0, 64.0)
  20.   do @player :: #@Player
  21.   do @player.x :: 96.0
  22.   do @player.y :: 800.0
  23.   do @player.width :: 46.0
  24.   do @player.height :: 116.0
  25.   do @player.toRight :: true
  26.   do draw@sampler(%point)
  27.   var pattern: []float :: [0.0, 64.0, 0.0, 128.0]
  28.  
  29.   while(wnd@act())
  30.     do @texBack.drawScale(0.0, 0.0, 1600.0, 900.0, 0.0, 0.0, 800.0, 450.0, draw@white)
  31.     if(input@pad(0, %left) >= 1)
  32.       do @player.veloX :- 0.8
  33.       do @player.toRight :: false
  34.     end if
  35.     if(input@pad(0, %right) >= 1)
  36.       do @player.veloX :+ 0.8
  37.       do @player.toRight :: true
  38.     end if
  39.     do @player.move(0.0)
  40.     do @texPlayer.drawScale(@player.x - 64.0, @player.y - 64.0, 128.0, 128.0, pattern[draw@cnt() / 12 % 4], @player.toRight ?(128.0, 0.0), 64.0, 64.0, draw@white)
  41.     for y(0, 14)
  42.       for x(0, 25)
  43.         var chip: int :: @map.get(x, y)
  44.         if(chip >= 0)
  45.           do @texMap.drawScale(x $ float * 64.0 - 32.0, y $ float * 64.0 - 32.0, 64.0, 64.0, chip $ float * 32.0, 0.0, 32.0, 32.0, draw@white)
  46.         end if
  47.       end for
  48.     end for
  49.     do draw@render(60)
  50.   end while
  51. end func
図5-1: kui_action5.kn
10~11行目で、マップ用のテクスチャとマップデータを入れる変数を作成し、18~19行目で実際に読みこんで格納しています。 11行目にあるgame@Mapクラスは、2Dマップを簡単に扱うための機能を持っています。
19行目の「game@makeMap」は、決められた書式で書かれたマップデータを読み込む関数です。 括弧内は順に「マップデータのファイルパス」「マップチップの幅」「マップチップの高さ」です。 マップチップの幅と高さは、マップとキャラクターの衝突判定をするときに使われます。
41~48行目でマップを描画しています。 以前説明したfor文を使い、41行目と48行目の「for y(0, 14)~end for」で0から14まで数え上げながら繰り返しています。 このfor文にはyと書いていますが、こうするとfor文の中でyを変数のようにアクセスすることができ、数え上げている最中の値を取得できます。
同様に、42行目と47行目の「for x(0, 25)~end for」でxを0から25まで数え上げながら繰り返しています。 従ってこの中では、yの値が初めは0でxの値が「0、1、2、…、25」となった後、yが1でxが「0、…、25」、yが2でxが「0、…、25」と進み、yが14でxが25になるまで繰り返される流れになります。
43~46行目ではこれらのxとyに対して各マップチップを描画し、マップ全体を描画しています。 43行目では@mapに入っているマップデータから、(x,y)の座標にあるマップチップを取得し、変数chipに入れています。 このint型とは、整数を入れる型です。 chipが0未満のときは「空気」を想定しているため何も描画せず、0以上のときにchipに応じた画像を45行目で描画しています。
45行目にある「$」の記号は型変換を意味し、int型の変数xに対して「x $ float」と書くと、整数であるxの値が小数であるfloat型に変換されて計算されます。 Kuinでは、int型の値は「5」のように書き、float型の値は「5.0」のように書かなければならないなど、型を厳密に区別しています。 異なる型でやりとりする必要があるときには「$」演算子などを使って変換してください。
実行すると図5-2のようになります。
マップの描画
図5-2: マップの描画
map_sample_side.txtの中身をテキストエディタで開いてみると、図5-3のようになっています。
map_sample_side.txt
図5-3: map_sample_side.txt
書式は、カンマと改行区切りで順に「1行(横)のマップチップの数」「1列(縦)のマップチップの数」と書き、以後マップチップの値を羅列します。 今回は横に26個、縦に15個並んでいます。 このような書式で書くとgame@makeMap関数で読み込めるようになります。

6ジャンプと衝突判定

最後に、プレイヤーのジャンプと衝突判定を実装します。 「while(wnd@act())」~「end while」の中身のみを変更しますので、その部分だけを抜粋します(図6-1)。
  1.   while(wnd@act())
  2.     do @texBack.drawScale(0.0, 0.0, 1600.0, 900.0, 0.0, 0.0, 800.0, 450.0, draw@white)
  3.     if(input@pad(0, %left) >= 1)
  4.       do @player.veloX :- 0.8
  5.       do @player.toRight :: false
  6.     end if
  7.     if(input@pad(0, %right) >= 1)
  8.       do @player.veloX :+ 0.8
  9.       do @player.toRight :: true
  10.     end if
  11.     if(input@pad(0, %a) >= 1 & input@pad(0, %a) <= 6 & @player.hitBottom())
  12.       do @player.veloY :: -14.0
  13.     end if
  14.     do @player.veloY :+ 0.4
  15.     do @player.veloX :: lib@clampFloat(@player.veloX, -8.0, 8.0)
  16.     do @player.veloY :: lib@clampFloat(@player.veloY, -14.0, 14.0)
  17.     do @player.move(0.0)
  18.     do game@hitMapRect(@map, @player, getChipInfo, null)
  19.     do @player.update()
  20.  
  21.     func getChipInfo(chip: int, info: game@ChipInfo)
  22.       switch(chip)
  23.       case lib@intMin to - 1
  24.         do info.shape :: %none
  25.       case 0, 1
  26.         do info.shape :: %rect
  27.         do info.solidFriction :: 0.2
  28.       case 2
  29.         do info.shape :: %triLeftTop
  30.         do info.solidFriction :: 0.1
  31.       case 6
  32.         do info.shape :: %rect
  33.         do info.solidFriction :: 0.0
  34.       case 7
  35.         do info.shape :: %rect
  36.         do info.solidFriction :: 0.2
  37.         do info.repulsion :: 0.95
  38.       end switch
  39.       do info.fluidFriction :: 0.98
  40.     end func
  41.  
  42.     do @texPlayer.drawScale(@player.x - 64.0, @player.y - 64.0, 128.0, 128.0, pattern[draw@cnt() / 12 % 4], @player.toRight ?(128.0, 0.0), 64.0, 64.0, draw@white)
  43.     for y(0, 14)
  44.       for x(0, 25)
  45.         var chip: int :: @map.get(x, y)
  46.         if(chip >= 0)
  47.           do @texMap.drawScale(x $ float * 64.0 - 32.0, y $ float * 64.0 - 32.0, 64.0, 64.0, chip $ float * 32.0, 0.0, 32.0, 32.0, draw@white)
  48.         end if
  49.       end for
  50.     end for
  51.     do draw@render(60)
  52.   end while
図6-1: kui_action6.kn
11~13行目はジャンプ処理です。 11行目の「&」は日本語の「かつ」に相当するものもので、このif文の条件は「Aボタンが押された時間が1フレーム以上で、かつAボタンが押された時間が6フレーム以下で、かつプレイヤーが接地していたらジャンプ」といった内容です。 接地する直前にボタンを押したときにもジャンプできるように、やや複雑な条件にしています。 12行目で上方向の速度を設定してジャンプしています。
14行目は下向きに速度を加算していますが、これは重力です。 15~16行目では最高速度を設定しており、lib@clampFloat関数は値を範囲内に収める関数で、括弧内は順に「対象の値」「最小値」「最大値」です。
18~19行目はプレイヤーとマップとの衝突判定を行っています。 衝突判定は、「move」「衝突判定の各関数」「update」の順で行います。 衝突判定の各関数を呼ぶと衝突結果のデータが蓄積され、update関数で位置や速度に反映される流れです。
18行目のgame@hitMapRectは、game@Mapとgame@Rectの衝突判定を行う関数で、括弧内は順に「game@Mapの値」「game@Rectの値」「マップチップ情報を返す関数」「衝突すると呼び出される関数」です。 ここではマップチップ情報を返す関数に、21~40行目で作っているgetChipInfo関数を渡していて、この中でマップチップの形状や摩擦や空気抵抗などの情報を返しています。
21行目のgetChipInfo関数は呼び出されるときに、括弧内のchipとinfoにそれぞれ「マップチップの種類」と「情報格納用の変数」が送られてきます。 chipの値を元に、infoに情報を格納する流れです。
22行目の「switch」は、括弧内の値に応じて分岐を行う命令で、値が合致した「case」の処理に飛びます。 例えば括弧内の値が2だった場合、28行目の「case 2」に合致するためそこからプログラムを実行して、31行目の別のcaseに達した段階でswitch文を抜けて39行目に飛びます。
caseには複数の値が書け、「1, 3, 5」のようにカンマ区切りで指定したり、「3 to 10」のように範囲指定もできます。 23行目の「lib@intMin」は、int型が扱える最小値を意味するため、「case lib@intMin to -1」とは、intの最小値以上-1以下の範囲を表します。
あとはinfoに入れる値ですが、info.shapeはマップチップの形状、info.solidFrictionは固体の摩擦係数、info.repulsionは触れたときの反発係数、info.fluidFrictionは空気抵抗となっています。 設定すると自動的に衝突時の物理挙動が計算されます。
実行すると図6-2のようになります。
アクションゲームの完成
図6-2: アクションゲームの完成
以上で、横視点アクションゲームの完成です。 重力を無くすと、上視点のゲームも作れます。 次回は、メニューの作り方やセーブデータの扱い方について説明します。
1734844983jaf