processingで音ゲーを作りたい! その1

経緯

最近友人が音ゲー制作を始めたらしい.俺も便乗する.

目標

よくあるレーン式の音ゲーを作る.例)ガルパ,deemoなど

音ゲーに必要な要素は?

音ゲーの動作に必要な情報は以下のものがあるだろう.

  • ノーツの流れてくるタイミング
  • ノーツの流れてくるレーン
  • ノーツの流れる速度
  • 曲の速さ(BPM

これらの情報はあらかじめ数値で指定できるとよい.

小節線を流す

今回はその1ということで1レーンだけ作ることにする.一気にやると死ぬので…

まず小節線を流してみよう.こんな感じで書いてみた.

<プログラム>

float speed,bpm,bs,lanewidth,tapY;
int beat,bars;

void setup(){
  size(640,360);
  stroke(255,255,255);
  lanewidth = width*0.1;//1レーンの太さ
  tapY = height*0.9;
  
  speed = 0.1;//ノーツの速度(1フレームあたりに進む量)
  bpm = 120;//曲の速さ
  beat = 4;//曲の拍子
  bs = (60/bpm)*1000*beat;//一小節あたりの秒数(マイクロ秒)
  bars = 100;//小節数
}

void draw(){
  background(0);
  drawbar(tapY);
  drawlane();
  drawbars(); 
}

void drawlane(){
  line((width-lanewidth)/2,0,(width-lanewidth)/2,height);
  line((width+lanewidth)/2,0,(width+lanewidth)/2,height);
}

void drawbars(){ 
  for(int i = 1;i <= bars;i++){
    float y = ttp(i*bs);
    if(0 < y && y < height){
      drawbar(y);
    }
  }
}

/*引数に「何秒後にtapYに描画するか」を指定すると
それに基づき現在のY座標を返す関数*/
float ttp(float seconds){
  float position = speed*(millis()-seconds)+tapY;
  return(position);
}

/*横線もとい小節線を引く*/
void drawbar(float y){
  push();
  stroke(200,200,50);
  translate(width/2,0);
  line(-lanewidth/2,y,lanewidth/2,y);
  pop();
}

<実行結果>

www.youtube.com

やっていることとしては以下の通りだ.

  1. ノーツのタイミングを指定する
  2. タイミングを秒数に変換する
  3. 描画

たったこれだけ.
ttp関数について解説.時間変化によるY座標の移動を関数で表してみよう.ノーツの速度は一定だから,変位のグラフは一次関数の形になる.よって,

Y座標 = 速度×時間+定数
Y = speed×t + b

t = secondsのときY = tapYになるから,それらを代入すると,

tapY = speed×seconds +b
b = tapY - speed×seconds

よって,

Y = speed×t - speed×seconds +tapY
Y = speed(t-seconds) + tapY

となる.processingにはプログラムを実行してからの時間を取得できるmillis()関数があるから,tの部分にはこれをいれればいい.

余談になるがttpというのはtime to positionの略.TOEIC300点なので間違っているかもしれない.

あと地味に便利な機能でpush()とpop()というのがある.座標を保存できるpushMatrix()とpopMatrix()というのもあるのだが,長い上に座標の情報しか保存しておけない.その点push()とpop()は関数名が短いし,線の色や太さなどの情報も記録しておけるのでおすすめだ.

ノーツを流す!

小節線を流せてしまえば正直こちらのもの.あとはこれをノーツに置き換えればいいだけだ.手始めにタップノーツクラスを作る.

<プログラム>

class tap{
  float timing;
  
  /*何小節目か,と1小節を何分割したうちの何番目のタイミングか,
  の情報からノーツが何秒後にくればいいかを計算する.*/
  tap(int bnum0,int tnum0,int snum0){
    timing = bs*((bnum0-1)+float(tnum0-1)/snum0);
  }
  
  void drawnote(){
    float y = ttp(timing);
    push();
    stroke(0,0,0);
    fill(255,255,255);
    translate(width/2,0);
    ellipse(0,y,lanewidth,lanewidth);
    pop();
  }
}

コンストラクタで初期値を決定する.bnum0はノーツのある小節番号,tnum/snumでその小節でのタイミングを表す.つまり,小節を何分割したうちの何番目のタイミングなのかということだ.小節の先頭を0とし,末尾を1として考えるので(tnum-1)/snumとなる.

次にこのクラスをオブジェクト化していく.

tap[] tap = new tap[2];

void setup(){
  tap[0] = new tap(3,1,4);
  tap[1] = new tap(3,2,4);
}

最後に実際の描画だ.bars関数の名前をdrawnotes関数にして,そのなかにノーツの描画に関するプログラムを書いていく.

void drawnotes(){ 
  for(int i = 1;i <= bars;i++){
    float y = ttp(i*bs);
    if(0 < y && y < height){
      drawbar(y);
    }
  }
  
  for(int i = 0;i < tap.length;i++){
    tap[i].drawnote();
  }
}

<実行結果>

www.youtube.com

譜面データを作ろう!

とりあえずノーツは流せたけどノーツの記述くそだるくね?ってことで譜面データを作れるようにしよう.processingではjsonが使えるからそれで作っていく.
まずjsonの記述をこんな感じにしよう.

{
  "スピード":0.1,
  "小節数":100,
  "BPM":120,
  "拍子":4,
  "タップ":[
    "4,1,4",
    "4,2,4",
    "4,3,4",
    "4,4,4"
  ]
}

ノーツのスピードや曲のbpmなどの情報もここから取得するようにしよう.次に実際にjsonを読み込んでいく.

JSONObject data;
JSONArray gettap;

void setup(){  
  data = loadJSONObject("譜面.json");
  
  speed = data.getFloat("スピード");//ノーツの速度(1フレームあたりに進む量)
  bpm = data.getFloat("BPM");//曲の速さ
  beat = data.getInt("拍子");//曲の拍子
  bars = data.getInt("小節数");//小節数

  gettap = data.getJSONArray("タップ");
  tap = new tap[gettap.size()];
  
  for(int i = 0;i < gettap.size();i++){
    String str = gettap.getString(i);
    int temp[] = int(split(str,","));
    tap[i] = new tap(temp[0],temp[1],temp[2]);
  }
}

こんな感じでおけ.あとは実行すればjsonに書いた個数のノーツが流れてくるよ.

次回は4レーンにしていきたい.

processingでのclassの使い方備忘録

1.classとは

・変数と関数が合体した奴

・オブジェクト化(インスタンス化)が必要

・配列にすることもできる

色々あるけど文じゃ説明できないのでとりあえずソース書く

<パックマンを描画する:その1>

class pacman{//クラスを作るよ!
  int x = width/2;
  int y = height/2;
  int size = 50;

  void drawpacman(){//パックマンを描画する関数
    arc(x,y,size,size,radians(45),radians(315));
  }
}

pacman pac1 = new pacman();//オブジェクト化
size(400,400);
pac1.drawpacman();

<実行結果>

f:id:Maryo:20201014225259p:plain

まあこんな感じでクラスは作れる。
流れとしては、

クラスを定義


オブジェクト化


クラスの外部からクラス内の関数を呼び出して使う

って感じ。
ただ、こんな処理にクラスを使う意味がないよね。
そこで、クラスにおいて重要な役割を担うやつを紹介しよう。

2.コンストラク

コンストラクタってのは何かっていうと、
オブジェクト化するときに実行される関数みたいなやつのこと。
記述方法は
クラス名(){}
って感じ。めっちゃ簡単だね!
で、基本的な使い方としては、
引数として与えられた値をクラス内の変数に受け渡すって感じ。
まあまたソース書いて説明しようかね。

<パックマンを描画する:その2>

class pacman{
  int x,y,size;
  
  pacman(int x0,int y0,int size0){//コンストラクタ
    x = x0;
    y = y0;
    size = size0;
  }
  
  void drawpacman(){
    arc(x,y,size,size,radians(45),radians(315));
  }
}

pacman pac1 = new pacman(60,80,100);//引数を指定
size(400,400);
pac1.drawpacman();

<実行結果>

f:id:Maryo:20201014231538p:plain

これでパックマンの位置とサイズを好きに指定できるようになった。
こんな風にオブジェクト化するときに引数を指定することで、同じクラスで違った挙動をさせることができる
でもこの程度の処理なら関数単体に引数を与えてやればできる。
というわけで次は、クラスでなきゃできないことをやってみよう。

3.いくつもオブジェクトを作る

今回はタイトルに書いてある通り、オブジェクトをいくつか作ってみよう。

<パックマンを描画する:その3>

class pacman{
  int x,y,size;
  
  pacman(int x0,int y0,int size0){
    x = x0;
    y = y0;
    size = size0;
  }
  
  void drawpacman(){
    arc(x,y,size,size,radians(45),radians(315));
  }
}

pacman pac1 = new pacman(60,80,100);
pacman pac2 = new pacman(280,100,50);
pacman pac3 = new pacman(300,300,200);
size(400,400);
pac1.drawpacman();
pac2.drawpacman();
pac3.drawpacman();

<実行結果>

f:id:Maryo:20201014233406p:plain

こんな感じで、それぞれ条件の違う3体のパックマンを召喚してみた。
今回、クラス内のソースは全く変えていない。
クラスを定義することで、微妙に条件が違う類似品大量生産できるというわけだ。
いうなれば、クラスというのは「寸法が記入されていない設計図」ってところかな。(いい例えかは分からんけど)
その設計図に寸法を入れて、型番を決めるという作業がオブジェクト化になるかと。
でもいちいち引数指定してオブジェクト化して…っていう作業は、数が増えてくるとめんどくさい。
配列みたいに扱えたら楽なのになあ…って思ったそこの君!できるんですよこれ。

4.オブジェクトの配列

というわけでこれが最終章。オブジェクトを配列として扱うよ。

<パックマンを描画する:その4>

class pacman{
  float x,y,size;//random()を使うためにfloat型にした
  
  pacman(float x0,float y0,float size0){
    x = x0;
    y = y0;
    size = size0;
  }
  
  void drawpacman(){
    arc(x,y,size,size,radians(45),radians(315),PIE);//見やすくするため輪郭をつけた
  }
}

pacman[] pacs = new pacman[100];//配列の宣言
size(400,400);
for(int i = 0;i < pacs.length;i++){
  pacs[i] = new pacman(random(20,380),random(0,400),random(10,50));//各オブジェクトに引数をランダムに指定
  pacs[i].drawpacman();//描画
}

<実行結果>

f:id:Maryo:20201015000920p:plain

うわぁきも。
以上、クラスを使うとこんな感じで大量生産できますよっていうお話でした。
本当はクラスはもっと奥が深いんですが、継承とかよくわからんのでやりません。
もしかしたら間違ってるところとかあるかもしれないですが大目に見ていただけるとありがたいです…。
因みにこのパックマンネタは公式のprocessingの教科書に載ってたからパクりましたすみません。
ソースは完全オリジナルなので許してください。
この備忘録がどっかの誰かの役に立てばいいなと思います。それでは。