GoogleAdsence

DXライブラリとC#での音楽ゲームの作り方その8 ステージ内での計算、描画処理

前回、初期化、計算と最適化について話をしたので、残りの計算と描画処理について説明します。
描画処理についてはオブジェ描画と小節の描画がありますが、今回はオブジェの描画のみです。

計算

  1. スペースキーを押して初めのループでなければゲーム終了→if (GetPadKey(“Start”) == 1 && !MG.FirstLoop)
  2. Aキーを押すとオートプレイのON/OFFの切り替え
  3. JとKキーを押すと小節の長さの倍率を変える

オートプレイON時

前回説明した曲の終了判定と同じ要領で11番目から7鍵盤+スクラッチ分ループしてNowCount >= bf.Timesの条件がtrueになった時に以下の処理をします。

  1. コンボ数を1足す
  2. コンボ数がMAXを超えてたらMAXを代入
  3. Great数を1足す
  4. 400より少ないなら、ゲージ数を1足す
  5. スコアをGREAT_SCORE分だけ足す
  6. フラグを消す
  7. グレイズとコンボエフェクトを消す
  8. BmsNumに次のループ開始位置を記録する
  9. BmsLoader.PlayMusicで音を鳴らす
  10. 鍵盤の後ろのフラッシュのエフェクトを出す
  11. グレイズの文字を出す
  12. コンボ数のエフェクトを出す
public class CStage : CScene
{
	  public override void Move()
    {
        float[] obj_x = { 55, 73, 87, 105, 119, 22, 0, 136, 150 };    // オブジェ表示X座標
~中略~
        // 終了処理
        if (GetPadKey("Start") == 1 && !MG.FirstLoop)
        {
            Life = 0;
            Terminate();
            return;
        }
        if (MG.FirstLoop == true) MG.FirstLoop = false;

        // オートプレイ
        if (CheckStateKey(DX.KEY_INPUT_A) == 1)
        {
            AutoPlay = !AutoPlay;
        }
        // スクロール幅変更
        if (CheckStateKey(DX.KEY_INPUT_J) > 0)
        {
            fScrMulti += 0.02f;
        }
        if (CheckStateKey(DX.KEY_INPUT_K) > 0)
        {
            fScrMulti -= 0.02f;
            if (fScrMulti < 0.05f)
                fScrMulti = 0.05f;
        }

        bool HitJudge = false;
        int se_data = 0;
        if (AutoPlay)
        {
            for (int j = 0; j < 9; j++)
            {
                for (int i = BmsNum[j + 11 + 20]; i < BmsLoader.GetObjeNum(j + 11); i++)
                {
                    CBmsData bf = BmsLoader.GetObje(j + 11, i);
                    if (NowCount < bf.Times)
                        break;
                    if (bf.Flag)
                    {
                        if (NowCount >= bf.Times)
                        {
                            // 当たり
                            MG.Combo++;
                            if (MG.Combo > MG.MaxCombo)
                                MG.MaxCombo = MG.Combo;
                            MG.Great++;
                            if (Gauge < 400) Gauge++;
                            MG.Score += GREAT_SCORE;
                            bf.Flag = false;    // フラグを消す
                            MG.Effect.RemoveAll(s => s.SEffectFlag); // グレイズとコンボエフェクトを消す
                            BmsNum[j + 11 + 20] = i + 1;
                            BmsLoader.PlayMusic(bf.iData);
                            MG.EffectBuf.Add(new CFlashEffect(obj_x[j], 80, 1, 20, j));
                            MG.EffectBuf.Add(new CGrazeEffect(EFFECT_POS_X, EFFECT_POS_Y, 0));
                            MG.EffectBuf.Add(new CComboEffect(EFFECT_POS_X, EFFECT_POS_Y, MG.Combo));
                        }
                    }
                }
            }
        }
        else
        {
            int[] a_index = { 0, 1, 2, 3, 4, 7, 8 };
            for (int i = 0; i < 7; i++)
            {
                if (CheckStatePad(KeyState[i]) == 1)
                {
                    HitJudge = SetSoundObje(a_index[i], NowCount, ref se_data);
                    BmsLoader.PlayMusic(se_data);
                    if (HitJudge) // 成功時の処理
                    {
                        //TToM("A");
                    }
                    else // 失敗時の処理
                    {
                        //TToM("B");
                    }
                }
            }
            // スクラッチ
            if (GetPadKey("Down") == 1 ||
                GetPadKey("Up") == 1)
            {
                HitJudge = SetSoundObje(5, NowCount, ref se_data);
                BmsLoader.PlayMusic(se_data);
                MG.EffectBuf.Add(new CFlashEffect(obj_x[5], 80, 1, 10, 9));
                if (HitJudge)
                {
                    //TToM("A");
                }
                else
                {
                    //TToM("B");
                }
            }
            // 見逃し判定
            for (int j = 0; j < 9; j++)
            {
                for (int i = BmsNum[j + 11 + 20]; i < BmsLoader.GetObjeNum(j + 11); i++)
                {
                    CBmsData bm = BmsLoader.GetObje(j + 11, i);
                    if (bm.Flag)
                    {
                        // オブジェが存在していて
                        if (bm.Times < (NowCount - 625))
                        {
                            // 見逃した場合はここでPOOR処理を行う
                            MG.Combo = 0;
                            MG.Score += POOR_SCORE;
                            MG.Poor++;
                            MG.Effect.RemoveAll(s => s.SEffectFlag); // グレイズとコンボエフェクトを消す
                            MG.EffectBuf.Add(new CGrazeEffect(EFFECT_POS_X, EFFECT_POS_Y, 3));
                            // オブジェを消す
                            bm.Flag = false;
                            // 判定オブジェをその次からに変更
                            BmsNum[j + 11 + 20] = i + 1;
                            break;
                        }
                    }
                }
            }
        }

        int ofset_score = MG.Score - TempScore;
        if (ofset_score > 10000)
        {
            TempScore += 500;
        }
        else if (ofset_score > 1000)
        {
            TempScore += 100;
        }
        else if (ofset_score > 500)
        {
            TempScore += 50;
        }
        else if (ofset_score > 0)
        {
            TempScore += 10;
            if (MG.Score - TempScore < 0)
            {
                TempScore = MG.Score;
            }
        }
    }
    void Terminate()
    {
        TempScore = 0;
        MG.Score = 0;
        MG.Combo = 0;
        MG.FirstLoop = true;
        MG.Effect.Clear();
        BmsLoader.Clear();
        MG.SceneBuf.Add(new CResult());
    }
~中略~
}

オートプレイOFF時

  1. if (CheckStatePad(KeyState[i]) == 1)押した瞬間に
  2. SetSoundObjeでタイミングの判定をして、trueならヒット、falseならミス判定を返す
  3. now_countとbm.Timesを比較して、graze_typeを決める。
  4. graze_type が-1(BAD)以外ならコンボ数を1足す
  5. コンボ数がMAXを超えてたらMAXを代入
  6. 400より少ないなら、ゲージ数を1足す
  7. スコアを graze_typeに対応した値の分だけ足す
  8. フラグを消す
  9. グレイズとコンボエフェクトを消す
  10. 新しくグレイズとコンボ数のエフェクトを出す
bool SetSoundObje(int obj_type, long now_count, ref int se_data)
{
    float[] obj_x = { 1, 18, 30, 46, 59, 92 };    // オブジェ表示X座標
    int graze_type = -1; // デフォルトはBAD
    for (int i = BmsNum[obj_type + 11 + 20]; i < BmsLoader.GetObjeNum(obj_type + 11); i++)
    {
        CBmsData bm = BmsLoader.GetObje(obj_type + 11, i);
        if (bm.Flag)
        {
            se_data = bm.iData;
            // オブジェが存在していて
            if ((now_count - 275) < bm.Times && bm.Times < (now_count + 275))
            { // GREAT
                graze_type = 0;
            }
            else if ((now_count - 475) < bm.Times && bm.Times < (now_count + 475))
            {   // GOOD
                graze_type = 1;
            }
            else if ((now_count - 675) < bm.Times && bm.Times < (now_count + 675))
            {   // BAD
                graze_type = 2;
            }
            if (graze_type != -1)
            {
                MG.Combo++;
                if (MG.Combo > MG.MaxCombo)
                    MG.MaxCombo = MG.Combo;
                if (Gauge < 400) Gauge++;
                bm.Flag = false; // オブジェを消す
                BmsNum[obj_type + 11 + 20] = i + 1;
                MG.Effect.RemoveAll(s => s.SEffectFlag);
                MG.EffectBuf.Add(new CComboEffect(EFFECT_POS_X, EFFECT_POS_Y, MG.Combo));
                switch (graze_type)
                {
                    case 0:
                        MG.EffectBuf.Add(new CGrazeEffect(EFFECT_POS_X, EFFECT_POS_Y, 0));
                        MG.Score += GREAT_SCORE;
                        MG.Great++;
                        break;
                    case 1:
                        MG.EffectBuf.Add(new CGrazeEffect(EFFECT_POS_X, EFFECT_POS_Y, 1));
                        MG.Score += GOOD_SCORE;
                        MG.Good++;
                        break;
                    case 2:
                        MG.EffectBuf.Add(new CGrazeEffect(EFFECT_POS_X, EFFECT_POS_Y, 2));
                        MG.Score += BAD_SCORE;
                        MG.Bad++;
                        break;
                }
                return true;
            }
        }
    }
    return false;
}

リソースのロード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DxLibDLL;
using System.Windows.Forms;
using static MusicGame.CInput;

namespace MusicGame
{
    public class CMusicGame
    {
        public int Score { set; get; } = 0;
        public int Great { set; get; } = 0;
        public int Good { set; get; } = 0;
        public int Bad { set; get; } = 0;
        public int Poor { set; get; } = 0;
        public int Combo { set; get; } = 0;
        public int MaxCombo { set; get; } = 0;
        public bool FirstLoop { set; get; } = true;
        private static CMusicGame MusicGame; // staticで変数を宣言
        public static CMusicGame MG
        {
            set { MusicGame = value; }
            get { return MusicGame; }
        }

        public List<CScene> Scene = new List<CScene>();
        public List<CScene> SceneBuf = new List<CScene>();
        public List<CEffect> Effect = new List<CEffect>();
        public List<CEffect> EffectBuf = new List<CEffect>();
        public uint[] Color = new uint[8];
        public int[] Font = new int[5];
        public int GraphHandle;
        public int ImgPlayer;
        public int[] ImgBar = new int[3];

        public int[][] ImgNum = new int[4][];

        public int[] ImgFlash = new int[6];
        public int[] ImgGraze = new int[4];
        public int ImgGauge;
        public int ImgResult;
        public void Initalize()
        {
            Color[0] = DX.GetColor(255, 255, 255);//白
            Color[1] = DX.GetColor(0, 0, 0);//黒
            Color[2] = DX.GetColor(255, 0, 0);//赤
            Color[3] = DX.GetColor(0, 255, 0);//緑
            Color[4] = DX.GetColor(0, 0, 255);//青
            Color[5] = DX.GetColor(255, 255, 0);//黄色
            Color[6] = DX.GetColor(0, 255, 255);//青緑
            Color[7] = DX.GetColor(255, 0, 255);//紫
            Font[0] = DX.CreateFontToHandle("HGPゴシックE", 15, 2, DX.DX_FONTTYPE_ANTIALIASING_EDGE);
            Font[1] = DX.CreateFontToHandle("HGPゴシックE", 20, 2, DX.DX_FONTTYPE_ANTIALIASING_EDGE);
            //タイトルで使用するフォント
            Font[2] = DX.CreateFontToHandle("HGPゴシックE", 50, 5, DX.DX_FONTTYPE_NORMAL);
            Font[3] = DX.CreateFontToHandle("HGPゴシックE", 30, 3, DX.DX_FONTTYPE_ANTIALIASING_EDGE);
            Font[4] = DX.CreateFontToHandle("HGPゴシックE", 18, 4, DX.DX_FONTTYPE_NORMAL);
            string[] sound_str = {"cursor.wav","kettei.wav","keteiA.mp3","keteiB.mp3" };
            SoundSE = new int[sound_str.Length];
            for (int i = 0; i < sound_str.Length; ++i)
            {
                SoundSE[i] = DX.LoadSoundMem($"Res/SE/{sound_str[i]}");
                if (SoundSE[i] == -1)
                {
                    MessageBox.Show($"Res/SE/{sound_str[i]}が読み込めません");
                }
            }
            GraphHandle = DX.LoadGraph("Res/Image/MainTip.png");
            if (GraphHandle == -1)
            {
                MessageBox.Show("Res/Image/MainTip.pngが読めこめません");
                return;
            }

            // 白いバー
            ImgBar[0] = DX.DerivationGraph(262, 86, 17, 3, GraphHandle);
            // 青いバー
            ImgBar[1] = DX.DerivationGraph(280, 86, 13, 3, GraphHandle);
            // 赤いバー
            ImgBar[2] = DX.DerivationGraph(229, 86, 32, 3, GraphHandle);

            // キーを押したときの後ろの光
            // 白
            ImgFlash[0] = DX.DerivationGraph(495, 83, 17, 240, GraphHandle);
            // 青
            ImgFlash[1] = DX.DerivationGraph(513, 83, 13, 240, GraphHandle);
            // 赤
            ImgFlash[2] = DX.DerivationGraph(463, 83, 32, 240, GraphHandle);

            for (int i = 0; i < 4; i++)
                ImgGraze[i] = DX.DerivationGraph(2, 270 + i * 30, 88, 28, GraphHandle);
            for (int i = 0; i < 4; i++)
                if (ImgGraze[i] == -1)
                    MessageBox.Show("ImgGraze画像ファイル読み込み失敗");

            ImgGauge = DX.DerivationGraph(0, 144, 200, 16, GraphHandle);

            for(int i = 0; i < ImgNum.Length; ++i)
            {
                ImgNum[i] = new int[10];
            }
            for (int i = 0; i < 10; i++)
                ImgNum[1][i] = DX.DerivationGraph(110 + i * 11, 84, 11, 14, GraphHandle);

            for (int i = 0; i < 10; i++)
                ImgNum[2][i] = DX.DerivationGraph(i * 11, 84, 11, 14, GraphHandle);

            for (int i = 0; i < 10; i++)
                ImgNum[3][i] = DX.DerivationGraph(i * 18 + 184, 270, 15, 25, GraphHandle);
            if (DX.LoadDivGraph("Res/Image/ScoreNum.png", 10, 10, 1, 17, 17, ImgNum[0]) == -1) MessageBox.Show("フォント用画像読み込み失敗");

            ImgResult = DX.LoadGraph("Res/Image/ResultF.bmp");
            if (ImgResult == -1)
                MessageBox.Show("Res/Image/ResultF.bmpの読み込みに失敗しました。");

            ImgPlayer = DX.LoadGraph("Res/Image/Main.png");
            if (ImgPlayer == -1)
                MessageBox.Show("Res/Image/Main.pngの読み込みに失敗しました。");

            Scene.Add(new CTitle());
        }

        public void MainLoop()
        {
            if (0 < SceneBuf.Count) { Scene.AddRange(SceneBuf); SceneBuf.Clear(); }
            if (0 < EffectBuf.Count) { Effect.AddRange(EffectBuf); EffectBuf.Clear(); }

            foreach (var effect in Effect.Where(x => 0 < x.Life)) effect.Move();
            foreach (var effect in Effect.Where(x => 0 < x.Life)) effect.Draw();
            
            foreach (var scene in Scene.Where(x => 0 < x.Life)) scene.Move();
            foreach (var scene in Scene.Where(x => 0 < x.Life)) scene.Draw();

            Scene.RemoveAll(s => s.Life <= 0);
            Effect.RemoveAll(s => s.Life <= 0);
            //CFpsControl.DrawFps(0, 465);
            //CFpsControl.FpsWait();
            // 裏画面の内容を表画面に反映する
            DX.ScreenFlip();
        }

        public void Destory()
        {
            
        }
        int[] SoundSE;
        public void PlaySound(int type)
        {
            if (DX.CheckSoundMem(SoundSE[type]) != 0)
            {
                DX.StopSoundMem(SoundSE[type]);
            }
            DX.PlaySoundMem(SoundSE[type], DX.DX_PLAYTYPE_BACK);
        }
    }
}

まず初めにGraphHandleにLoadGraphで以下の画像を読み込みます。
その後DerivationGraphでImgBar(白、青、赤)のバーやImgFlash (白、青、赤) のフラッシュ、ImgGraze(GREATE,GOOD,BAD,POOR)、ImgGauge(ゲージ)を分解してそれぞれの変数に読み込みます。

描画処理

続いて読み込んだデータを描画します。

public class CStage : CScene
{
~中略~
    // 判定ラインを上げる JUDGE_LINEを減らす
    // 判定ラインを下げる JUDGE_LINEを増やす
    const int JUDGE_LINE = 317;
    const int MOVIE_START_POS_X = 168;
    const int MOVIE_START_POS_Y = 0;
    const int MOVIE_END_POS_X = 640 - MOVIE_START_POS_X;
    const int MOVIE_END_POS_Y = 375;
    const int EFFECT_POS_X = 100;
    const int EFFECT_POS_Y = 200;
    const int POOR_SCORE = -50;
    const int BAD_SCORE = 50;
    const int GOOD_SCORE = 100;
    const int GREAT_SCORE = 200;
    public override void Draw()
    {
        int[] obj_kind = { 0, 1, 0, 1, 0, 2, 0, 1, 0 };           // オブジェの種類
        float[] obj_x = { 55, 73, 87, 105, 119, 22, 0, 136, 150 };    // オブジェ表示X座標
                                                                      // 動画再生フラグがオンなら
        if (MovieStartFlag)
        {
            // ムービー映像を画面いっぱいに描画
            DX.DrawExtendGraph(MOVIE_START_POS_X, MOVIE_START_POS_Y, MOVIE_END_POS_X, MOVIE_END_POS_Y, BmsLoader.GetMovieHandle(), 0);
        }

        // アニメーション表示
        if (ImageID > 0 && !MovieStartFlag)
        {
            DX.DrawExtendGraph(MOVIE_START_POS_X, MOVIE_START_POS_Y, MOVIE_END_POS_X, MOVIE_END_POS_Y, BmsLoader.GetGraphicHandles(ImageID), 1);
        }

        DX.DrawGraph(0, 0, MG.ImgPlayer, 1);

        double buf = 0.0;
        int gauge;
        gauge = Gauge / 4;
        buf = Gauge < 400 ? Math.Sin(Time / 3) * 8.0 : Math.Sin(Time / 3) * 8.0;
        Time++;
        // ゲージの描画
        DX.DrawRectGraph(23, 393, 0, 0, Gauge / 2 + (int)buf, 16, MG.ImgGauge, 1, 0);

        for (int i = 0; i < 3; i++) // ゲージのパーセンテージを描画
        {
            DX.DrawRotaGraph(212 - 15 * i, 352, 1.20f, 0.0f, MG.ImgNum[1][gauge % 10], 1);
            gauge /= 10;
            if (gauge == 0) break;
        }

        int score = TempScore;
        for (int i = 0; i < 6; i++) // スコアを描画
        {
            DX.DrawRotaGraph(132 - 16 * i, 428, 1.20f, 0.0f, MG.ImgNum[2][score % 10], 1);
            score /= 10;
            if (score == 0) break;
        }
        
        int bpm = (int)BmsLoader.GetNowBpm();
        for (int i = 0; i < 3; i++) // BPMを描画
        {
            DX.DrawRotaGraph(67 - 16 * i, 455, 1.15f, 0.0f, MG.ImgNum[2][bpm % 10], 1);
            bpm /= 10;
            if (bpm == 0) break;
        }

        // 小節
        for (int i = BmsNum[0]; i < BmsLoader.GetBarCount(); i++)
        {
            CBarInfo bar = BmsLoader.GetBar(i);
            int yy = (int)((double)(bar.Times - NowCount) / BMSDATA_RESOLUTION * (int)(fScrMulti * 192));
            // 判定ラインより下なら次回からその次の小節から参照する
            if (yy < 0)
                BmsNum[0] = i + 1;
            // 画面の上より外ならばその先は全て描画スキップ
            if (yy > JUDGE_LINE - 1 + 2)
                break;
            // 画面内なら描画
            DX.DrawString(170, JUDGE_LINE - yy + 2, $"{i}", DX.GetColor(0, 0, 255));
            //DrawFormatString(170, JUDGE_LINE - yy + 2, DX.GetColor(0, 0, 255), "%d", i);
            DX.DrawLine(22, JUDGE_LINE - yy + 2, 170, JUDGE_LINE - yy + 2, DX.GetColor(255, 255, 255), 1);
        }
        // オブジェ
        for (int j = 0; j < 9; j++)
        {
            int size = BmsLoader.GetObjeNum(j + 11);
            for (int i = BmsNum[j + 11]; i < size; i++)
            {
                CBmsData bm = BmsLoader.GetObje(j + 11, i);
                int yy = (int)((double)(bm.Times - NowCount) / BMSDATA_RESOLUTION * (int)(fScrMulti * 192));
                // 判定ラインより下なら次回からその次のオブジェから参照する
                if (yy < 0)
                    BmsNum[j + 11] = i + 1;
                // 画面の上より外ならばその先は全て描画スキップ
                if (yy > JUDGE_LINE - 1 + 6)
                    break;
                // 画面内なら描画
                DX.DrawGraphF(obj_x[j], (float)(JUDGE_LINE - (int)(yy + 0.5f)), MG.ImgBar[obj_kind[j]], 1);

            }
        }
        string auto_play_str = AutoPlay ? "ON" : "OFF";
        DX.DrawString(250, 400, $"オートプレイ {auto_play_str}", DX.GetColor(255, 255, 255));
    }
}

ソースコードを簡単に説明すると

  1. MovieStartFlagがtrueならDrawExtendGraphで動画を再生
  2. MovieStartFlagがfalseでImageIDが0より多いなら画像をDrawExtendGraphで描画
  3. DrawGraphでプレイヤー本体を描画
  4. DrawRectGraphでゲージを描画
  5. DrawRotaGraphでスコアとBPMを描画
  6. 小節とオブジェの描画

スコアとBPMの詳しい描画方法についてはこちらで説明してます。

2D描画の奥行きについて (オブジェ描画)

一般的に2Dでの描画とは後から表示したものが上に来ます。

これは例えば紙にシールを貼っていくようなイメージで、2Dの描画も紙のようなバックバッファに対して画像をどんどん描画してくと、前に描画した画像はさらに上から描画された画像によって上書きされてしまいます。

なんだか難しいことを書いているようですが、このサイトの音ゲーではこれが重要となる箇所が1箇所あります。それは白鍵(白)と黒鍵(青)のオブジェの表示です。

下の画像を見てください。

①と②は重なっていないため描画順は特に関係ありませんが、問題は③で同じタイミングに白と青が重なっています。

普通プログラムを作るとしたらオブジェクトを表示するため鍵盤の左側から、「白→青→白→青→白→スクラッチ」という順番でforを使って回すことを考えると思います。

これはオブジェのチャンネル番号が11~16とちょうど連番になっていることも理由ですが、このままだと実際には以下の③のように青の方が先に描画されるため結果的に奥に行ってしまいます。

※分かりやすいように黄色にしてみました。

これでは見た目的にもおかしいので、これに対応した描画を行う必要があります。

と、ここまで過剰に説明してとても難しそうに見えますが、実は解決法は以外に簡単です。

このゲームの仕様をよく考えてみると青2つは必ず白より上に表示されます。また、ここではそれより上に描画されるオブジェはありません。ということは先に白3つを描画してから、あとで青2つを描画することにしてしまえばよいのです。


つまり「白→白→白→青→青→スクラッチ」という感じです。ついでにこれをチャンネル番号的に見ると、「11→13→15→12→14→16」となります。

1つのチャンネル内オブジェを全て表示してみる

ここでははじめから6チャンネル分の描画を考えるのではなく、最初は1チャンネル分の描画について考えます。

なんでも一気に考えようとすると頭がこんがらがってくるので、出来るだけ基本的なところから考えるというのがプログラミングのコツです。

ではまず一番左側の白鍵(チャンネル11番)の描画を考えてみましょう。
※以下はチャンネル11番のレーンを表しています

1つのチャンネル内には当然オブジェが1つしかないわけがありません。つまりたくさんのオブジェがそのチャンネルに存在するわけなので、これらを画面上に全て表示しなければなりません。

なおここでは画面上に描くと言っていますが、ゲーム自体はオブジェが上からスクロールしてくるため、
実際にはこの画面よりさらに上にも描画をしていることになります。

本当は画面外にいったら描画を行わないのが良いのですが、とりあえずここでは最適化は考えずこのチャンネルを全て描画するプログラムを考えてみます。

ここで使用するのはお馴染みのCBmsProクラスのGetObjeNumとGetObjeです。そして座標を算出するのに必要なのが各オブジェのBMSカウント値で、これはオブジェクラスのTimesに入っています。

Times が0の時というのはゲーム画面上で言うと判定バーの上部となります。
ちなみにサンプルではこの座標はx=1、y=413としています。

この位置を時間0の時の原点位置とし、ここをベースとして上方向にオブジェを順番に表示していきます。

さて Times の値が0の場合とは最初の小節(0小節目)の先頭となり、上の画像の原点の座標ということになりますが、それでは Times が9600、つまり1小節目の先頭だった場合を考えてみましょう。

ちなみに Times の値をそのままピクセル数として使ってしまうと、このオブジェは原点から9600ピクセル上に表示されることになってしまいます。※ゲームとしてスクロールするようになるとものすごい速さで落ちてきます(笑)

そこでここではゲームの見易さなどを考慮して1小節を192ピクセルと定義し、これに合わせるためように倍率を掛けることにします。

すると以下のように1小節ごとに192ピクセル区切りの見やすい画面になります。

それでは1小節を192ピクセルとする場合に計算式を求めてみましょう。

まずlTimeはBMSカウント単位で記録されています。そしてBMSカウント値というのは1小節あたり9600と定義しています。

そして今回は9600という値を192ピクセルで表示したいわけなので、「9600÷192px」で得られた値「50」が求められます。

そしてこの50を各オブジェの Times から割ってあげることで、1小節を192ピクセルとした最終的なピクセル数に変換出来ます。

さて、画面上では上に行くほど時間的に後のほうのオブジェが表示されます。本来2D画面というのは左上が原点で右下に行くほど値が大きくなりますが、実際にオブジェを表示する際は上方向に行くために符号を反転してやる必要があります。

これらを踏まえ全てのオブジェを原点から上方向に表示するプログラムは以下のようになります

for( i=0;i< BmsLoader .GetObjeNum(0x11);i++ ) {
    CBmsData b = BmsLoader.GetObje( 0x11, i );
    float off_y = (float)b->Times / (BMS_RESOLUTION / 192);
    DX.DrawGraphF(15, (float)( 413-off_y ), MG.ImgBar[obj_kind[j]], 1);
}

注意点として一番左側のチャンネルは11番となりますが、ここでは11をそのまま使うのではなく16進数として扱います。このためGetObjeNumとGetObjeに渡すチャンネル番号は、実際には「0x11」と指定しなければなりません。 (CBmsLoderクラスはチャンネル番号を16進数として解析しているため)

ではこの処理について詳しく説明しますが、まずはこのチャンネル内の全オブジェを対象とするためforループを構築しています。

次にそれぞれのオブジェの情報をGetObjeから取得します。

そして上で説明した式を使ってオブジェのカウント値からピクセル数に変換した値を計算します。

なお、ここでは計算した値はテンポラリ変数のoff_yに入りますが、この値はまだプラス値のためそのままでは画面的に下方向に向かっていることになるので、最後に原点(Y=413)から上方向になるように、413から先ほど計算したピクセル値を引くことで、上方向の表示に変換されます。

ちなみに描画はDrawGraphFを利用していますが、このサンプルではCStageコンストラクタにてDerivationGraphで切り抜きMG.ImgBar[0]に白鍵用オブジェを設定しているので、ここではそれを指定しています。

1つ重要な問題として、上記のプログラムはoff_yはfloat型になっていますが、これはDrawGraphFの引数がfloat型となっているためそれに合わせたものですが、以降に説明するスクロール処理にて、オブジェと小節ラインの計算をする際に、実はfloat誤差によりオブジェと小節ラインがうまく重ならずにチラチラとブレてしまうことがあるため、ここでは計算結果を一度int型の整数として算出してから、DrawGraphFに渡す時にfloatにキャストして渡しています。

for( i=0;i< BmsLoader .GetObjeNum(0x11);i++ ) {
    CBmsData b =  BmsLoader .GetObje( 0x11,i );
    int obj_y = (int)((double)b->Times / (BMS_RESOLUTION / 192));
    DX.DrawGraphF(15, (float)( 413-off_y ), MG.ImgBar[0], 1); 
}

ひとまずこれでこのチャンネル内のオブジェは全て表示されたことになります。

全てのチャンネルを表示してみる

1つのチャンネルの表示方法が分かったところで、これを応用して全てのチャンネルを表示してみます。

上記のプログラムではチャンネル番号を0x11に固定にしていました。
つまりこれを動的に変えることで、全てのチャンネル(11~16)の表示を行うことが出来ます。

まず最初に考えられるのは、以下のように多重forとして定義することです。

    for( j=0;j<6;j++ ) {
        for( i=0;i< BmsLoader.GetObjeNum(0x11+j);i++ ) {
            CBmsData b =  BmsLoader .GetObje( 0x11+j,i );
            :
        }
    }

しかしこれだと最初に説明した通り、後から描いたものが手前に表示されるため、
譜面によっては黒鍵盤のあとに白鍵盤が表示されてしまいます。

これに対応するためこのforに細工をすることにします。

重なりを正すには描画する順番を変えればよいと説明しましたが、
具体的にはこの描画する順番を別に定義しておき、それを参照することで順番を間接的に変えることが出来ます。

上のプログラムだと「白(0)→青(1)→白(2)→青(3)→白(4)→スクラッチ(5)」という順番になりますが、
これを「白(0)→白(2)→白(4)→青(1)→青(3)→スクラッチ(5)」となるように、
まずは以下のようなインデックスリストを定義しておきます。

static const int index[6] = { 0,2,4,1,3,5 }; // インデックスリスト

そしてGetObjeNumとGetObjeにはこのインデックスリストを参照することで、表示順に合わせたチャンネル番号が参照出来るようになります。

以下はこの処理を組み込んだプログラムです。

for( j=0;j<6;j++ ) {
    for( i=0;i<BmsLoader.GetObjeNum(0x11+index[j]);i++ ) {
             CBmsData b = BmsLoader.GetObje( 0x11+index[j],i );
            
    }
}

頭の回転が速い人はこのindex配列に直接「0x11,0x13,0x15,0x12,0x13,0x16」と、チャンネル番号を入れておけば良いんじゃないかと思うかもしれません。しかしまだ説明していませんがこの変数はこれ以外にも用途があるため、ここでは敢えてチャンネル番号ではなく0からの番号としています。

チャンネルごとのX座標とオブジェ画像

上で全てのチャンネルに対して描画を行うための土台は揃いましたが、実はこれだとまだ問題があります。
それはチャンネルごとに表示する座標が異なるのと、表示するオブジェ画像自体が異なるということです。

例えばレーンの幅が固定幅であれば単純に掛け算すれば求めることが出来ますが、このゲームはレーンの幅がチャンネルごとに異なっています。
またそれに合わせて白鍵、黒鍵、スクラッチの3つの画像が必要ですが、上のプログラムではとりあえず白鍵画像が表示されるようにしかなっていませんでした。

ということで上のプログラムにこれらの情報を取り込む方法としては、インデックスリストと同じくチャンネルごとにX座標とオブジェの種類を配列として用意し、これを参照することでレーンごとに座標とオブジェ画像を変えるように制御します。

ちなみにこれらの配列はindex変数の順番とは関係なく、普通に「白→青→白→青→白→スクラッチ」として定義します。

まず各レーンのX座標ですが、ここでは以下のように定義しています。

またオブジェの種類ですが、ここでは以下のように定義しています。

0白鍵
1黒鍵
2スクラッチ

これは切り抜き画像の順番と同一となっています。
※ID15、16、17が白鍵、黒鍵、スクラッチ

そしてこれらを実際に配列として定義すると以下のようになります。

    static const int obj_kind[6]    = { 0,1,0,1,0,2 };              // オブジェの種類
    static const float obj_x[6]     = { 1,18,30,46,59,92 };         // オブジェ表示X座標

画像の切り抜きIDは15からとなるため、この配列を参照して表示させる場合はこれに+15します。
※直接配列を「15、16、15、16、15、17」とすることも出来ますが、
 これもあとで流用するためここでは敢えてオフセット値としています

これを使って実際に表示を行う場合、これもインデックスリストの順番で参照されなければなりません。
つまり「白→白→白→青→青→スクラッチ」の順に参照するためには、先にインデックスリストから今回の参照先を求めるような参照の参照という形になっています。

そしてこれらを全て含めたプログラムは以下のようになります。

for( j=0;j<6;j++ ) {
    for( i=0;i<BmsLoader.GetObjeNum(0x11+index[j]);i++ ) {
         CBmsData b =  BmsLoader .GetObje( 0x11+index[j],i );
        int obj_y = (int)((double)b->lTime / (BMS_RESOLUTION / 192));
        DX.DrawGraphF(obj_x[j], (float)(JUDGE_LINE - (int)(yy + 0.5f)), MG.ImgBar[obj_kind[j]], 1)
    }
}

これで全てのオブジェが画面上(実際には見えない部分にも)に表示されたことになります。