製作中の音ゲー紹介と誰でも作れる音ゲーの作り方(誰でも作れるとは言ってない)

こんにちは、こまつなです。

この記事は、CCS Advent Calendar 2016の記事です。

人によってははじめましての方もいるかもしれませんが、一応CCS3年でプログラマを担当しております。

今回、私は現在製作中の音ゲーと、音ゲーの簡単な製作方法(とんでもなく適当)について書いていきたいと思います。

読みたい人だけ読もう。割と長いから。

1,製作中の音ゲーについて


まず、現在製作中の音ゲーについて。
以下の画像をご覧下さい。
MUSIC=OPERATOR プレー画面
2つのレーンにそれぞれ左は「L」、右は「R」のノーツが降ってきます。
ゲームパッドのLRボタンに対応します。

それに加えて、レーンがそれぞれ移動します。
以下の画像をご覧下さい。
MUSIC=OPERATOR レーン移動ノーツ
右のレーンが右へ移動したあと、すぐに戻るようにノーツが置いてあります。
これは、ゲームパッドのアナログスティック、または、十字・○△□×ボタンに対応します。
レーンのみ、アナログスティックで操作する感覚です。

こんな感じのゲームを現在製作中なのですが、3D風にするため、レーンの端が切れてたり、かなり苦労している状態です。
なんとかC91のCCSのディスクに入れられるよう頑張りたいと思います。

今後は、C92で1サークルとして出たいと思っています。


ここで、1つ問題があり、現在収録曲が全く集められていない状況です。
譜面制作ツールはのちのち制作するとして、とりあえず収録曲が必要なのです。

冬コミ版に入れるのは難しいですが、C92以降でどなたか協力していただける方、@starealnightにリプライを送るか、starealnight@gmail.comにメールを送っていただけると助かります。
よろしくお願いします。


2, 誰でも作れる音ゲーの作り方

こっちがメインなはず()

音ゲーの作り方と言ってもそこまで詳しく説明するわけではありませんのでご了承ください。


今回は、弐寺やポップンなどのように、上からノーツが降ってくる&判定ラインで判定するようなゲームを作るということにしましょう。

やることは大きく分けて3つ。
①譜面の読み込み
②ノーツの設置(動き含む)
③判定


順番に説明しましょうかね。


①譜面の読み込み

かなり重要です。これを一番最初にやらないと詰む。というか後からやると実装不可能になる可能性すらある。
「音ゲー 作り方」でぐぐるとそれなりに出てくるが、そういうのはとりあえず置いといて、最低限の要素だけ取り込む方法をやってみましょう。

譜面で最低限必要な情報は
・BPM
・開始時間(ズレ)
・ノーツ(開始小節、開始拍、ノーツ種類、ロング長さ、降ってくるレーン)

こんなもんですね。他にもBPM変化とか変拍子とかも実装しようとすればできなくはないので、やりたい人は自分で考えてみましょう。

じゃあまず譜面データを読み込みます。
譜面データはなんでもいいですが、今回は一般的なCSVデータから読み込みます。


#include <stdio.h>
#define MAX_READ 2000
#define MAX_TYPE 5


FILE *fp;
char s[MAX_READ][MAX_TYPE];
char str[100];
int ret;
float f0, f1, f2, f3, f4;

typedef struct{
 int ret;
 double bpm;
 double data[MAX_READ][MAX_TYPE];
 double start_time[MAX_READ], notes_long[MAX_READ], displacement, sub_long[MAX_READ];
 int notes_type[MAX_READ], notes_rane[MAX_READ];
 int rane_totalcount[MAX_READ], rane_count[5];
        int max_combo;

}BODY_HUMEN_t;
BODY_HUMEN_t humen;


int split(char *str, const char *delim, char *outlist[]) {
 char    *tk;
 int     cnt = 0;

 tk = strtok(str, delim);
 while (tk != NULL && cnt < MAX_TYPE) {
  outlist[cnt++] = tk;
  tk = strtok(NULL, delim);
 }
 return cnt;
}

void humen_loading(char *fname){//譜面読み込み用関数

 fp = fopen(fname, "r");
 if (fp == NULL){
  //ここに読み込めなかった時の処理。強制終了等。
 }
 

 while ((ret = fscanf(fp, "%f,%f,%f,%f,%f", &f0, &f1, &f2, &f3, &f4)) != EOF){
  if (humen.ret == 0){//bpm
   humen.bpm = f0;
   humen.displacement = f1;
  }else{
   humen.data[humen.ret - 1][0] = f0;
   humen.data[humen.ret - 1][1] = f1;
   humen.data[humen.ret - 1][2] = f2;
   humen.data[humen.ret - 1][3] = f3;
   humen.data[humen.ret - 1][4] = f4;
  }
  humen.ret++;
 }
 for (int i = 0; i < humen.ret; i++) {
  humen.start_time[i] = (double)humen.displacement + 14400 / humen.bpm*((humen.data[i][0] - 1) + (humen.data[i][1] - 1) / 16);
  humen.notes_rane[i] = (int)humen.data[i][2] - 1;
  humen.notes_type[i] = (int)humen.data[i][3];
  if (humen.data[i][4] == 0){
   humen.notes_long[i] = 0;
  }
  else{
   humen.notes_long[i] = 14400 / humen.bpm / 16 * humen.data[i][4];
  }

  if (humen.notes_type[i] == 1){
   humen.max_combo++;
  }
  else if (humen.notes_type[i] == 2){
   humen.max_combo += 2;
  }
  for (int j = 0; j < 5; j++){
   if (humen.notes_rane[i] == j){
    humen.rane_totalcount[i] = humen.rane_count[j];
    humen.rane_count[j]++;
   }
  }
 }

 fclose(fp);
}

説明をしていくと、まずFILE *fpというものにファイルのアドレスをぶち込み、fopenで開く。
そして、while ((ret = fscanf(fp, "%f,%f,%f,%f,%f", &f0, &f1, &f2, &f3, &f4)) != EOF){...のところで、各データを列ごとに読み込んでいく。
今回の場合、1行目にBPMと開始時間(ズレ)を読み込み、humen.bpmとhumen.displacementに代入している。
それ以降はノーツデータ(開始小節、開始拍、ノーツ種類、ロング長さ、降ってくるレーン)をhumen.data[現在の行数][列数]に代入している。

その後のfor(int i = 0; i < humen.ret; i++) {...以降では、代入したデータを元に、開始カウントを計算、レーンやノーツ種やロング時間もそれぞれ計算している。
また、レーン毎にノーツの数も計算している。

これでとりあえず読み込みはできた。あくまで例なので、ここおかしいとかはあまり言わないで欲しい。だって動いてるもの。
譜面データの一部
1行目にBPMとズレ、2行目以降に譜面データがある。
基本的に1行1ノーツで書いていくとんでもない作業。

②ノーツの設置(動き含む)

次にノーツを実際に置いていこう。
ノーツを置くといっても、すでに①でノーツの降ってくる時間を速度で掛ければいいだけじゃん?
距離=速度×時間じゃん?1秒間に60フレームなんだからあとは計算でなんとかなるよね。

そう思ってた時期が僕にもありました。

1秒間に60フレーム?とんでもない。実際はずれるずれる。どんどんノーツがずれていく。
正直かなり驚きました。今までの概念を覆された気分。

ではどうするか。
時間を実際に取得します。

#include <windows.h>

LARGE_INTEGER start_time;
LARGE_INTEGER now_time;
LARGE_INTEGER now_freq;
LARGE_INTEGER ini_freq;

int counter;

void game_player(void){
 if (counter == 0){
  start_time.QuadPart = (LONGLONG)GetNowHiPerformanceCount();
 }

 now_time.QuadPart = (LONGLONG)GetNowHiPerformanceCount();
 counter = (int)((now_time.QuadPart - start_time.QuadPart) / 1000 / 16.6666666666666667);
}

これは実際の1部ですが、すでによくわからないことをしているかと思われます。
今回使っているのはGetNowHiPerformanceCount();
これは、PCの機械的な稼働時間を計算しているものなので、かなり高精度に取得をしてくれます。

LARGE_INTEGERについては、これを使わないとGetNowHiPerformanceCount();の値が取得できないと考えてください。理由は詳しくは話しません。

まず、カウンタが0の時に初期値をstart_time.QuadPartに代入します。
その後、毎フレーム現在時刻を取得し、カウンタにフレームに変換した値を代入します。

これだけです。

そして、

humen.y[i] = (判定ライン) - (humen.start_time[i] - counter)*humen.bpm*speed;



human.y[i]は各ノーツの座標を、speedは譜面速度を表します。
時間×速度で譜面の座標を決定します。

これだけでとりあえずノーツは降ってきます。次は判定です。

③判定の実装

こればかりは実際にコードを乗せるととんでもない行数になってしまうので、各自考えてください。
判定実装で注意すべきことは4つ。

・キーを押した時間を取得し、判定ノーツをレーンごとに処理
・ノーツが判定時間から一定時間以内にある場合にのみ処理する(空POOR対策)
・1フレーム内でそのノーツを処理したらそのレーンの処理は終わりで次のレーンに移動
・必ず見逃し時の判定も作ること






上げるとキリないきがするのでこれくらいで。

変数はお好きなものを作ってください。

C++?そんな高度なもの僕ができるわけないじゃないですか…。

あとわからなくなったらじゃんじゃんググろう、誰かに聞こう。

習うより慣れろ!(適当)



以上です。適当感満載だけど書けただけよかった。

わからないことあったら聞いてくれれば相談に乗ります。余裕があったらね!

ちなみに一番時間かかるの譜面製作だから!マジで!

1譜面につき2時間とか余裕でかかるからね!!!


ありがとうございました!

次はいなえのまきです(おでんゲーの作り方を教えてくれるらしい)

コメント

このブログの人気の投稿

幸せに生きるってなんだろう

2016年 熱海旅行 2日目