2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Diagnostics;
7 using SharpDX.DirectInput;
10 using FDK.メディア.サウンド.WASAPI;
16 using FDKUtilities = FDK.FDKUtilities;
38 public const float ヒット判定バーの中央Y座標 = 869f + 43f;
40 public const float レーンフレームの左端位置 = 619f;
42 public ConcurrentDictionary<ヒットランク種別, int> ヒットランク別ヒット回数
45 } = new ConcurrentDictionary<ヒットランク種別, int>();
50 this.子リスト.Add( this._ステージ台 = new 画像( @"$(System)images\ステージ台.png" ) );
51 this.子リスト.Add( this._レーンフレーム = new レーンフレーム() );
52 this.子リスト.Add( this._ドラムセット = new ドラムセット() );
53 this.子リスト.Add( this._ヒット判定バー = new 画像( @"$(System)images\判定バー.png" ) );
54 this.子リスト.Add( this._チップ画像 = new 画像( @"$(System)images\Chips.png" ) );
55 this.子リスト.Add( this._回転羽 = new 回転羽( 最大同時発火数: 32 ) );
56 this.子リスト.Add( this._コンソールフォント = new コンソールフォント() );
57 this.子リスト.Add( this._ドラムサウンド = new ドラムサウンド() );
58 this.子リスト.Add( this._コンボ = new コンボ() );
59 this.子リスト.Add( this._ヒットランク = new ヒットランク() );
60 this.子リスト.Add( this._クリア時フェードアウト = new フェードアウト( 1.0f ) );
61 this.子リスト.Add( this._白パネル = new 画像( @"$(System)images\パネル白64x64.png" ) );
62 this.子リスト.Add( this._FPS = new FPS() );
65 protected override void On活性化( グラフィックデバイス gd )
67 using( Log.Block( FDKUtilities.現在のメソッド名 ) )
69 this._活性化した直後である = true;
70 this._背景動画開始済み = false;
71 this._現在進行描画中の譜面スクロール速度の倍率 = App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率;
75 //this._デコード済みWaveSource = null; --> キャッシュなので消さない。
76 this._背景動画開始済み = false;
77 this._BGM再生開始済み = false;
78 this._チップ画像の矩形リスト = new 矩形リスト( @"$(System)images\Chips Rectangle List.xml" ); // デバイスリソースは持たないので、子Activityではない。
80 this.ヒットランク別ヒット回数.Clear();
81 foreach( ヒットランク種別 hitRankType in Enum.GetValues( typeof( ヒットランク種別 ) ) )
82 this.ヒットランク別ヒット回数[ hitRankType ] = 0;
84 #region " 背景動画とBGMを生成する。"
86 if( ( null != App.演奏スコア ) && ( App.演奏スコア.背景動画ファイル名.Nullでも空でもない() ) )
88 Log.Info( "背景動画とBGMを読み込みます。" );
91 this.子リスト.Add( this._背景動画 = new 動画( App.演奏スコア.背景動画ファイル名, App.システム設定.動画デコーダのキューサイズ ) );
93 // 動画から音声パートを抽出して BGM を作成。
94 if( ( null != this._デコード済みWaveSource ) && this._デコード済みWaveSource.Path.Equals( App.演奏スコア.背景動画ファイル名 ) )
96 // (A) 前回生成したBGMとパスが同じなので、前回のデコード済み WaveSource をキャッシュとして再利用する。
97 Log.Info( "前回生成したサウンドデータを再利用します。" );
101 // (B) 初めての生成か、または前回生成したBGMとパスが違うので、新しくデコード済み WaveSource を生成する。
102 this._デコード済みWaveSource?.Dispose();
103 this._デコード済みWaveSource = new DecodedWaveSource( App.演奏スコア.背景動画ファイル名, App.サウンドデバイス.WaveFormat );
106 this._BGM?.Dispose();
107 this._BGM = App.サウンドデバイス.サウンドを生成する( this._デコード済みWaveSource );
111 Log.Info( "背景動画とBGMはありません。" );
116 #region " 最初のフェーズを設定する。"
118 if( ( App.ビュアーモードではない ) ||
119 ( ( null != App.最後に取得したビュアーメッセージ ) &&
120 ( App.最後に取得したビュアーメッセージ.種別 == Viewer.ViewerMessage.指示種別.演奏開始 ) ) )
122 this.現在のフェーズ = フェーズ.演奏中;
126 this.現在のフェーズ = フェーズ.ビュアーメッセージ待機;
133 protected override void On非活性化( グラフィックデバイス gd )
135 using( Log.Block( FDKUtilities.現在のメソッド名 ) )
137 // 背景動画を生成した場合は子リストから削除。
138 if( null != this._背景動画 )
139 this.子リスト.Remove( this._背景動画 );
141 App.ユーザ管理.選択されているユーザ.保存する();
143 this._活性化した直後である = false;
147 public override void 高速進行する()
149 Debug.Assert( this.活性化している );
151 switch( this.現在のフェーズ )
161 if( this._活性化した直後である )
163 this._活性化した直後である = false;
164 this._描画開始チップ番号 = 0; // -1 → 0; 演奏開始。
165 this._チップアニメ.開始する( 最初の値: 0, 最後の値: 48, 値をひとつ増加させるのにかける時間ms: 10 );
167 #region " 演奏開始時刻を初期化し、動画とBGMの再生を開始する。"
169 double 演奏開始位置の先頭からの時間sec = 0.0;
172 App.サウンドタイマ.リセットする( 演奏開始位置の先頭からの時間sec );
173 Log.Info( $"演奏開始時刻(背景動画再生チェック前): {App.サウンドタイマ.現在時刻sec} sec" );
175 // 演奏開始時刻の設定(2) ビュアーメッセージがある場合、開始時刻を修正する。
176 var msg = App.最後に取得したビュアーメッセージ;
180 演奏開始位置の先頭からの時間sec = this._演奏開始小節番号を設定しその時刻secを返す( msg.演奏を開始する小節番号 );
181 Log.Info( $"演奏開始の先頭からの時間: {演奏開始位置の先頭からの時間sec} sec" );
184 App.システム設定.Autoチップのドラム音を再生する = msg.ドラムチップのヒット時に発声する;
187 // BGMと動画を(必要あれば)再生開始。
188 this._再生中の時刻なら動画とBGMを再生開始する( 演奏開始位置の先頭からの時間sec );
190 // 演奏開始時刻の設定(3) 動画とBGMが再生された場合、ここに到達するまでに多少の遅延が生じているので、ここで演奏開始時刻を再取得しておく。
191 App.サウンドタイマ.リセットする( 演奏開始位置の先頭からの時間sec );
192 Log.Info( $"演奏開始時刻(背景動画再生チェック後): {App.サウンドタイマ.現在時刻sec} sec" );
199 this._FPS.FPSをカウントしプロパティを更新する();
201 #region " 背景動画が再生されているのにBGMがまだ再生されていないなら、すぐに再生を開始する。"
203 if( this._背景動画開始済み && !( this._BGM再生開始済み ) )
206 this._BGM再生開始済み = true;
211 double 現在の演奏時刻sec = this._演奏開始からの経過時間secを返す();
217 this._描画範囲のチップに処理を適用する( 現在の演奏時刻sec, ( chip, index, ヒット判定バーとの時間sec, ヒット判定バーとの距離 ) => {
219 var オプション設定 = App.ユーザ管理.選択されているユーザ.オプション設定;
220 var 対応表 = オプション設定.ドラムとチップと入力の対応表[ chip.チップ種別 ];
221 var AutoPlay = オプション設定.AutoPlay[ 対応表.AutoPlay種別 ];
223 bool チップはヒット済みである = chip.ヒット済みである;
224 bool チップはMISSエリアに達している = ( ヒット判定バーとの時間sec > オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] );
225 bool チップはヒット判定バーを通過した = ( 0 <= ヒット判定バーとの距離 );
233 if( チップはMISSエリアに達している )
236 if( AutoPlay && 対応表.AutoPlayON.MISS判定 )
238 this._チップのヒット処理を行う( chip, ヒットランク種別.MISS, 対応表.AutoPlayON.自動ヒット時処理 );
241 else if( !AutoPlay && 対応表.AutoPlayOFF.MISS判定 )
243 this._チップのヒット処理を行う( chip, ヒットランク種別.MISS, 対応表.AutoPlayOFF.ユーザヒット時処理 );
252 if( チップはヒット判定バーを通過した )
255 if( AutoPlay && 対応表.AutoPlayON.自動ヒット )
257 this._チップのヒット処理を行う( chip, ヒットランク種別.AUTO, 対応表.AutoPlayON.自動ヒット時処理 );
260 else if( !AutoPlay && 対応表.AutoPlayOFF.自動ヒット )
262 this._チップのヒット処理を行う( chip, ヒットランク種別.AUTO, 対応表.AutoPlayOFF.自動ヒット時処理 );
277 App.入力管理.すべての入力デバイスをポーリングする( 入力履歴を記録する: false );
282 var オプション設定 = App.ユーザ管理.選択されているユーザ.オプション設定;
284 #region " ヒットしてようがしてまいが起こすアクション(空打ち処理)。 "
286 foreach( var 入力 in App.入力管理.ポーリング結果 )
288 if( 入力.InputEvent.離された )
289 continue; // 押下イベントじゃないなら無視。
291 this._ドラムセット.ヒットアニメ開始( 入力.Type, オプション設定.表示レーンの左右 );
293 var カラム = オプション設定.ドラムとチップと入力の対応表.対応表.Where( ( kvp ) => ( kvp.Value.ドラム入力種別 == 入力.Type ) ); // カラムは struct なので FirstOrDefault() は使わない。
294 if( 0 < カラム.Count() )
295 this._レーンフレーム.フラッシュ開始( カラム.First().Value.表示レーン種別 );
300 var 処理済み入力 = new List<ドラム入力イベント>(); // ヒット処理が終わった入力は、二重処理しないよう、この中に追加しておく。
302 this._描画範囲のチップに処理を適用する( 現在の演奏時刻sec, ( chip, index, ヒット判定バーとの時間sec, ヒット判定バーとの距離 ) => {
309 var チップの対応表 = オプション設定.ドラムとチップと入力の対応表[ chip.チップ種別 ];
311 if( オプション設定.AutoPlay[ チップの対応表.AutoPlay種別 ] )
312 return; // チップが AutoPlay ON である。
314 if( !( チップの対応表.AutoPlayOFF.ユーザヒット ) )
315 return; // このチップは、AutoPlay OFF 時でもユーザヒットの対象ではない。
317 double ヒット判定バーとの時間の絶対値sec = Math.Abs( ヒット判定バーとの時間sec );
319 bool チップはMISSエリアに達している = ( ヒット判定バーとの時間sec > オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] );
320 if( !( ヒット判定バーとの時間sec >= -( オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] ) && !チップはMISSエリアに達している ) )
321 return; // チップはヒット可能エリアにある。
323 var ヒット入力 = App.入力管理.ポーリング結果.FirstOrDefault( ( 入力 ) => {
324 #region " chip にヒットする入力があれば true を返す。"
326 if( !( 入力.InputEvent.押された ) )
327 return false; // 押下入力じゃない。
329 if( 処理済み入力.Contains( 入力 ) )
330 return false; // すでに今回のターンで処理済み(=処理済み入力リストに追加済み)。
332 // chip がシンバルフリーの対象なら、chip に直接対応する入力の他にも、ヒット判定すべき入力がある。
333 if( チップの対応表.シンバルフリーの対象 && オプション設定.シンバルフリー )
336 var カラムs = オプション設定.ドラムとチップと入力の対応表.対応表.Where( ( kvp ) => ( kvp.Value.ドラム入力種別 == 入力.Type ) );
337 foreach( var カラム in カラムs )
339 // シンバルフリーなチップがあるなら true。
340 if( カラム.Value.シンバルフリーの対象 )
347 // chip に対応する入力なら true。
348 return ( チップの対応表.ドラム入力種別 == 入力.Type );
360 #region " ヒットランクを判定する。"
362 var ヒットランク = ヒットランク種別.POOR;
364 if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.PERFECT ] )
366 ヒットランク = ヒットランク種別.PERFECT;
368 else if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.GREAT ] )
370 ヒットランク = ヒットランク種別.GREAT;
372 else if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.GOOD ] )
374 ヒットランク = ヒットランク種別.GOOD;
379 this._チップのヒット処理を行う( chip, ヒットランク, チップの対応表.AutoPlayOFF.ユーザヒット時処理 );
388 if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Escape ) )
390 #region " ESC → ステージキャンセル "
392 if( App.ビュアーモードではない )
395 this.現在のフェーズ = フェーズ.キャンセル;
400 // ビュアーモード時のキャンセルは無効。
405 if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Up ) )
407 #region " 上 → 譜面スクロールを加速 "
409 const double 最大倍率 = 8.0;
410 App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 =
411 Math.Min( App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 + 0.5, 最大倍率 );
415 if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Down ) )
417 #region " 下 → 譜面スクロールを減速 "
419 const double 最小倍率 = 0.5;
420 App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 =
421 Math.Max( App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 - 0.5, 最小倍率 );
425 foreach( var ev in App.入力管理.MIDI入力デバイス.入力イベントリスト.Where( ( ie ) => ( 255 == ie.Key ) ) )
429 this._ドラムセット.ハイハットのベロシティ = ev.Velocity;
440 public override void 進行描画する( グラフィックデバイス gd )
442 Debug.Assert( this.活性化している );
443 Debug.Assert( null != gd );
445 switch( this.現在のフェーズ )
448 if( this._活性化した直後である )
449 break; // 進行処理がまだ行われていない。
451 #region " 譜面スクロール速度が変化している → 追い付き進行 "
454 double 倍率 = this._現在進行描画中の譜面スクロール速度の倍率;
456 if( 倍率 < App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 )
458 if( 0 > this._スクロール倍率追い付き用_最後の値 )
460 this._スクロール倍率追い付き用カウンタ = new LoopCounter( 0, 1000, 10 ); // 0→100; 全部で10×1000 = 10000ms = 10sec あれば十分だろう
461 this._スクロール倍率追い付き用_最後の値 = 0;
465 while( this._スクロール倍率追い付き用_最後の値 < this._スクロール倍率追い付き用カウンタ.現在値 )
468 this._スクロール倍率追い付き用_最後の値++;
471 this._現在進行描画中の譜面スクロール速度の倍率 =
472 Math.Min( 倍率, App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 );
475 else if( 倍率 > App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 )
477 if( 0 > this._スクロール倍率追い付き用_最後の値 )
479 this._スクロール倍率追い付き用カウンタ = new LoopCounter( 0, 1000, 10 ); // 0→100; 全部で10×1000 = 10000ms = 10sec あれば十分だろう
480 this._スクロール倍率追い付き用_最後の値 = 0;
484 while( this._スクロール倍率追い付き用_最後の値 < this._スクロール倍率追い付き用カウンタ.現在値 )
487 this._スクロール倍率追い付き用_最後の値++;
490 this._現在進行描画中の譜面スクロール速度の倍率 =
491 Math.Max( 倍率, App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 );
496 this._スクロール倍率追い付き用_最後の値 = -1;
497 this._スクロール倍率追い付き用カウンタ = null;
503 this._コンボ.進行描画する( gd );
504 this._回転羽.進行描画する( gd );
506 double 演奏時刻sec = this._演奏開始からの経過時間secを返す();
508 #region " 背景動画とBGMの進行描画。"
512 // 背景動画チップがヒット済みなら、背景動画の進行描画を行う。
513 switch( App.ユーザ管理.選択されているユーザ.オプション設定.動画表示パターン種別 )
515 case 動画表示パターン種別.中央表示:
519 float w = gd.設計画面サイズ.Width;
520 float h = gd.設計画面サイズ.Height;
522 this._背景動画?.描画する( gd, new RectangleF( 0f, 0f, w, h ), 0.2f ); // 全体
527 // 進行描画せず、直前に取得したフレームをそのまま描画する。
528 this._背景動画?.前のフレームを描画する( gd, new RectangleF(
529 w * ( 1f - 拡大縮小率 ) / 2f,
530 h * ( 1f - 拡大縮小率 ) / 2f - 上移動,
538 case 動画表示パターン種別.最大表示:
541 this._背景動画?.描画する( gd, new RectangleF( 0f, 0f, gd.設計画面サイズ.Width, gd.設計画面サイズ.Height ), 1.0f );
547 // 動画が重たいかもしれないので、演奏時刻をここで更新しておく。 ---> 重たくても更新禁止!(譜面スクロールがガタつく原因になる)
548 //演奏時刻sec = this._演奏開始からの経過時間secを返す();
553 this._ステージ台.描画する( gd, 0f, 0f );
554 this._レーンフレーム.進行描画する( gd, レーンフレームの左端位置 );
555 this._コンボ.進行描画する( gd );
556 this._ヒットランク.進行描画する( gd, レーンフレームの左端位置 );
557 this._小節線拍線を描画する( gd, 演奏時刻sec );
558 this._ヒット判定バー.描画する( gd, 597f, ヒット判定バーの中央Y座標 - 43f );
559 this._ドラムセット.進行描画する( gd );
560 this._チップを描画する( gd, 演奏時刻sec );
561 this._回転羽.進行描画する( gd );
562 this._FPS.VPSをカウントする();
563 this._FPS.描画する( gd, 0f, 0f );
566 case フェーズ.クリア時フェードアウト:
567 if( this._クリア時フェードアウト.開始されていない )
569 this._クリア時フェードアウト.開始する();
573 if( this._クリア時フェードアウト.完了した )
575 this.現在のフェーズ = フェーズ.クリア;
578 this._ステージ台.描画する( gd, 0f, 0f );
579 this._レーンフレーム.進行描画する( gd, レーンフレームの左端位置 );
580 this._ヒット判定バー.描画する( gd, 597f, ヒット判定バーの中央Y座標 - 43f );
581 this._ドラムセット.進行描画する( gd );
582 this._回転羽.進行描画する( gd );
583 this._クリア時フェードアウト.進行描画する( gd, this._白パネル );
584 this._FPS.VPSをカウントする();
585 this._FPS.描画する( gd, 0f, 0f );
591 case フェーズ.ビュアーメッセージ待機:
596 public void 演奏を停止する()
598 using( Log.Block( FDKUtilities.現在のメソッド名 ) )
600 this._描画開始チップ番号 = -1; // 演奏停止
603 this._背景動画開始済み = false;
605 this._コンボ.COMBO値 = 0;
610 /// 演奏クリア時には、次の結果ステージに入ってもBGMが鳴り続ける。
611 /// そのため、後からBGMだけを別個に停止するためのメソッドが必要になる。
613 public void BGMを停止する()
615 using( Log.Block( FDKUtilities.現在のメソッド名 ) )
618 this._BGM?.Dispose();
621 //this._デコード済みWaveSource?.Dispose(); --> ここではまだ解放しない。
622 //this._デコード済みWaveSource = null;
626 public void BGMのキャッシュを解放する()
628 using( Log.Block( FDKUtilities.現在のメソッド名 ) )
631 FDKUtilities.解放する( ref this._デコード済みWaveSource );
636 private double _現在進行描画中の譜面スクロール速度の倍率 = 1.0;
639 /// <see cref="スコア.チップリスト"/> のうち、描画を始めるチップのインデックス番号。
643 /// 演奏開始直後は 0 で始まり、対象番号のチップが描画範囲を流れ去るたびに +1 される。
644 /// このメンバの更新は、高頻度進行タスクではなく、進行描画メソッドで行う。(低精度で構わないので)
646 private int _描画開始チップ番号 = -1;
648 private 動画 _背景動画 = null;
651 /// 停止と解放は、演奏ステージクラスの非活性化後に、外部から行われる。
652 /// <see cref="SST.ステージ.演奏.演奏ステージ.BGMを停止する"/>
653 /// <see cref="SST.ステージ.演奏.演奏ステージ.BGMのキャッシュを解放する"/>
655 private Sound _BGM = null;
658 /// BGM の生成もとになるデコード済みサウンドデータ。
661 /// 活性化と非活性化に関係なく、常に最後にデコードしたデータを持つ。(キャッシュ)
662 /// 演奏ステージインスタンスを破棄する際に、このインスタンスもDisposeすること。
664 private DecodedWaveSource _デコード済みWaveSource = null;
666 private bool _背景動画開始済み = false;
668 private bool _BGM再生開始済み = false;
670 private 画像 _ステージ台 = null;
672 private レーンフレーム _レーンフレーム = null;
674 private ドラムセット _ドラムセット = null;
676 private 画像 _ヒット判定バー = null;
678 private 画像 _チップ画像 = null;
680 private 矩形リスト _チップ画像の矩形リスト = null;
682 private LoopCounter _チップアニメ = new LoopCounter();
684 private 回転羽 _回転羽 = null;
686 private コンソールフォント _コンソールフォント = null;
688 private ドラムサウンド _ドラムサウンド = null;
690 private コンボ _コンボ = null;
692 private ヒットランク _ヒットランク = null;
694 private FPS _FPS = null;
696 private LoopCounter _スクロール倍率追い付き用カウンタ = null;
698 private int _スクロール倍率追い付き用_最後の値 = -1;
700 private フェードアウト _クリア時フェードアウト = null;
702 private 画像 _白パネル = null;
704 private bool _活性化した直後である;
710 /// 指定した小節の先頭を演奏開始位置として設定し、その位置(時刻)を返す。
712 /// <returns>演奏開始位置sec。(0~)</returns>
713 private double _演奏開始小節番号を設定しその時刻secを返す( int 演奏開始小節番号 )
715 var score = App.演奏スコア;
720 double 演奏開始時刻sec = 0.0;
722 for( int i = 0; i < score.チップリスト.Count; i++ )
724 if( score.チップリスト[ i ].小節番号 < 演奏開始小節番号 )
726 // 開始チップ以前のチップはヒット済みとする。
727 score.チップリスト[ i ].ヒット済みの状態にする();
734 // 演奏開始時刻は、開始チップの発声時刻から少し早めの値に。
735 演奏開始時刻sec = score.チップリスト[ i ].発声時刻sec;
738 // 開始チップ以降のすべてのチップをヒット前の状態にする。
739 for( int j = i; j >= 0; j-- )
741 if( score.チップリスト[ j ].ヒット済みである )
742 score.チップリスト[ j ].ヒット前の状態にする();
752 private void _再生中の時刻なら動画とBGMを再生開始する( double 時刻sec )
754 using( Log.Block( FDKUtilities.現在のメソッド名 ) )
756 Log.Info( $"指定された時刻: {時刻sec} sec" );
758 var 背景動画チップ = App.演奏スコア.チップリスト.FirstOrDefault( ( chip ) => ( chip.チップ種別 == チップ種別.背景動画 ) );
760 if( null == 背景動画チップ )
762 Log.Info( "背景動画チップは存在しません。" );
766 double 背景動画の長さsec = this._BGM?.長さsec ?? 0.0; // 動画のサイズは、映像ではなく音声を優先する。
768 Log.Info( $"背景動画の発生時刻: {背景動画チップ.発声時刻sec} sec" );
769 Log.Info( $"背景動画の長さ: {背景動画の長さsec} sec" );
772 if( ( 背景動画チップ.発声時刻sec <= 時刻sec ) && ( 時刻sec < ( 背景動画チップ.発声時刻sec + 背景動画の長さsec ) ) )
775 double 再生開始時刻sec = ( 時刻sec - 背景動画チップ.発声時刻sec );
776 this._背景動画?.再生を開始する( 再生開始時刻sec );
777 this._背景動画開始済み = true;
778 this._BGM?.Play( 再生開始時刻sec );
779 this._BGM再生開始済み = true;
780 Log.Info( $"背景動画の再生を開始しました。(再生開始時刻: {再生開始時刻sec} sec)" );
784 Log.Info( $"指定された時刻は背景動画の再生期間内ではないので、何もしません。" );
789 private double _演奏開始からの経過時間secを返す()
791 return App.サウンドタイマ.現在時刻sec;
794 // 小節線・拍線 と チップ は描画階層(奥行き)が異なるので、別々のメソッドに分ける。
796 private void _小節線拍線を描画する( グラフィックデバイス gd, double 現在の演奏時刻sec )
798 if( null == this._チップ画像 )
801 this._チップ画像.加算合成 = false;
803 this._描画範囲のチップに処理を適用する( 現在の演奏時刻sec, ( chip, index, ヒット判定バーとの時間sec, ヒット判定バーとの距離 ) => {
805 if( chip.チップ種別 == チップ種別.小節線 )
809 float 左位置 = レーンフレームの左端位置 + レーンフレーム.レーンto横中央相対位置[ 表示レーン種別.LeftCrash ] - 1f; // -1f はレーン線の幅の半分。
810 float 上位置 = (float)( ヒット判定バーの中央Y座標 + ヒット判定バーとの距離 - 1f ); // -1f は小節線の厚みの半分。
811 if( this._チップ画像の矩形リスト[ nameof( チップ種別.小節線 ) ] is RectangleF 画像範囲 )
813 this._チップ画像.描画する( gd, 左位置, 上位置, 転送元矩形: 画像範囲, 描画先矩形を整数境界に合わせる: true ); // false にすると、
814 this._チップ画像.描画する( gd, 左位置 + 画像範囲.Width, 上位置, 転送元矩形: 画像範囲, 描画先矩形を整数境界に合わせる: true ); // チカチカする。
819 else if( chip.チップ種別 == チップ種別.拍線 )
823 float 左位置 = レーンフレームの左端位置 + レーンフレーム.レーンto横中央相対位置 [ 表示レーン種別.LeftCrash ] - 1f; // -1f はレーン線の幅の半分。
824 float 上位置 = (float)( ヒット判定バーの中央Y座標 + ヒット判定バーとの距離 - 1f ); // -1f は拍線の厚みの半分。
825 if( this._チップ画像の矩形リスト[ nameof( チップ種別.拍線 ) ] is RectangleF 画像範囲 )
827 this._チップ画像.描画する( gd, 左位置: 左位置, 上位置: 上位置, 転送元矩形: 画像範囲, 描画先矩形を整数境界に合わせる: true ); // false にすると、
828 this._チップ画像.描画する( gd, 左位置: 左位置 + 画像範囲.Width, 上位置: 上位置, 転送元矩形: 画像範囲, 描画先矩形を整数境界に合わせる: true ); // チカチカする。
837 private void _チップを描画する( グラフィックデバイス gd, double 現在の演奏時刻sec )
839 if( null == this._チップ画像 )
842 this._チップ画像.加算合成 = false;
844 this._描画範囲のチップに処理を適用する( 現在の演奏時刻sec, ( chip, index, ヒット判定バーとの時間sec, ヒット判定バーとの距離 ) => {
846 float 縦中央位置 = (float)( ヒット判定バーの中央Y座標 + ヒット判定バーとの距離 );
848 #region " チップが描画開始チップであり、かつ、そのY座標が画面下端を超えたなら、描画開始チップ番号を更新する。"
850 if( ( index == this._描画開始チップ番号 ) &&
851 ( gd.設計画面サイズ.Height + 40.0 < 縦中央位置 ) ) // +40 はチップが隠れるであろう適当なマージン。
855 if( App.演奏スコア.チップリスト.Count <= this._描画開始チップ番号 )
857 this.現在のフェーズ = フェーズ.クリア時フェードアウト;
858 this._描画開始チップ番号 = -1; // 演奏完了。
868 #region " チップを個別に描画する。"
870 float 音量0to1 = chip.音量 / (float) チップ.最大音量;
874 case チップ種別.LeftCrash:
875 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ nameof( チップ種別.LeftCrash ) ], 縦中央位置, 音量0to1 );
878 case チップ種別.HiHat_Close:
879 _アニメチップを1つ描画する( 表示レーン種別.HiHat, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Close ) ], 縦中央位置, 音量0to1 );
882 case チップ種別.HiHat_HalfOpen:
883 _アニメチップを1つ描画する( 表示レーン種別.HiHat, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Close ) ], 縦中央位置, 音量0to1 );
884 _単画チップを1つ描画する( 表示レーン種別.Foot, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_HalfOpen ) ], 縦中央位置, 1.0f );
887 case チップ種別.HiHat_Open:
888 _アニメチップを1つ描画する( 表示レーン種別.HiHat, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Close ) ], 縦中央位置, 音量0to1 );
889 _単画チップを1つ描画する( 表示レーン種別.Foot, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Open ) ], 縦中央位置, 1.0f );
892 case チップ種別.HiHat_Foot:
893 _単画チップを1つ描画する( 表示レーン種別.Foot, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Foot ) ], 縦中央位置, 1.0f );
897 _アニメチップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare ) ], 縦中央位置, 音量0to1 );
900 case チップ種別.Snare_ClosedRim:
901 _単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare_ClosedRim ) ], 縦中央位置, 1.0f );
904 case チップ種別.Snare_OpenRim:
905 _単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare_OpenRim ) ], 縦中央位置, 音量0to1 );
907 //_単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare ) ], 縦中央位置, 音量0to1 );
910 case チップ種別.Snare_Ghost:
911 _単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare_Ghost ) ], 縦中央位置, 1.0f );
915 _アニメチップを1つ描画する( 表示レーン種別.Bass, this._チップ画像の矩形リスト[ nameof( チップ種別.Bass ) ], 縦中央位置, 音量0to1 );
919 _アニメチップを1つ描画する( 表示レーン種別.Tom1, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom1 ) ], 縦中央位置, 音量0to1 );
923 _単画チップを1つ描画する( 表示レーン種別.Tom1, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom1_Rim ) ], 縦中央位置, 1.0f );
927 _アニメチップを1つ描画する( 表示レーン種別.Tom2, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom2 ) ], 縦中央位置, 音量0to1 );
931 _単画チップを1つ描画する( 表示レーン種別.Tom2, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom2_Rim ) ], 縦中央位置, 1.0f );
935 _アニメチップを1つ描画する( 表示レーン種別.Tom3, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom3 ) ], 縦中央位置, 音量0to1 );
939 _単画チップを1つ描画する( 表示レーン種別.Tom3, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom3_Rim ) ], 縦中央位置, 1.0f );
942 case チップ種別.RightCrash:
943 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ nameof( チップ種別.RightCrash ) ], 縦中央位置, 音量0to1 );
947 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Chinaは左 )
949 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftChina" ], 縦中央位置, 音量0to1 );
953 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightChina" ], 縦中央位置, 音量0to1 );
958 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Rideは左 )
960 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftRide" ], 縦中央位置, 音量0to1 );
964 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightRide" ], 縦中央位置, 音量0to1 );
969 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Rideは左 )
971 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftRide_Cup" ], 縦中央位置, 音量0to1 );
975 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightRide_Cup" ], 縦中央位置, 音量0to1 );
980 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Splashは左 )
982 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftSplash" ], 縦中央位置, 音量0to1 );
986 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightSplash" ], 縦中央位置, 音量0to1 );
990 case チップ種別.LeftCymbal_Mute:
991 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftCymbal_Mute" ], 縦中央位置, 1.0f );
994 case チップ種別.RightCymbal_Mute:
995 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightCymbal_Mute" ], 縦中央位置, 1.0f );
1005 void _単画チップを1つ描画する( 表示レーン種別 lane, RectangleF? 元矩形, float 上位置, float 音量0to1 )
1010 var 画像範囲 = (RectangleF) 元矩形;
1014 左位置: レーンフレームの左端位置 + レーンフレーム.レーンto横中央相対位置[ lane ] - ( 画像範囲.Width / 2f ),
1015 上位置: 上位置 - ( ( 画像範囲.Height / 2f ) * 音量0to1 ),
1019 void _アニメチップを1つ描画する( 表示レーン種別 lane, RectangleF? 画像範囲orNull, float Y, float 音量0to1 )
1021 if( null == 画像範囲orNull )
1024 var 画像範囲 = (RectangleF) 画像範囲orNull;
1026 float チップ1枚の高さ = 18f;
1027 画像範囲.Offset( 0f, this._チップアニメ.現在値 * 15f ); // 下端3pxは下のチップと共有する前提のデザインなので、18f-3f = 15f。
1028 画像範囲.Height = チップ1枚の高さ;
1029 float 左位置 = レーンフレームの左端位置 + レーンフレーム.レーンto横中央相対位置[ lane ] - ( 画像範囲.Width / 2f );
1030 float 上位置 = Y - ( チップ1枚の高さ / 2f ) * 音量0to1;
1032 this._チップ画像?.描画する( gd, 左位置, 上位置, 転送元矩形: 画像範囲, Y方向拡大率: 音量0to1 );
1039 /// <see cref="_描画開始チップ番号"/> から画面上端にはみ出すまでの間の各チップに対して、指定された処理を適用する。
1041 /// <param name="適用する処理">引数は、順に、対象のチップ、チップ番号、ヒット判定バーとの時間sec、ヒット判定バーとの距離</param>
1042 private void _描画範囲のチップに処理を適用する( double 現在の演奏時刻sec, Action<チップ, int, double, double> 適用する処理 )
1044 var スコア = App.演奏スコア;
1048 for( int i = this._描画開始チップ番号; ( 0 <= i ) && ( i < スコア.チップリスト.Count ); i++ )
1050 var チップ = スコア.チップリスト[ i ];
1052 // ヒット判定バーとチップの間の、時間 と 距離 を算出。→ いずれも、負数ならバー未達、0でバー直上、正数でバー通過。
1053 double ヒット判定バーとの時間sec = 現在の演奏時刻sec - チップ.描画時刻sec;
1054 double ヒット判定バーとの距離 = スコア.指定された時間secに対応する符号付きピクセル数を返す( this._現在進行描画中の譜面スクロール速度の倍率, ヒット判定バーとの時間sec );
1057 bool チップは画面上端より上に出ている = ( ( ヒット判定バーの中央Y座標 + ヒット判定バーとの距離 ) < -40.0 ); // -40 はチップが隠れるであろう適当なマージン。
1058 if( チップは画面上端より上に出ている )
1061 // 処理実行。開始判定(描画開始チップ番号の更新)もこの中で。
1062 適用する処理( チップ, i, ヒット判定バーとの時間sec, ヒット判定バーとの距離 );
1066 private void _チップのヒット処理を行う( チップ chip, ヒットランク種別 hitRankType, ドラムとチップと入力の対応表.Column.Columnヒット処理 ヒット処理表 )
1068 chip.ヒット済みである = true;
1072 #region " チップを再生する。"
1074 if( chip.チップ種別 == チップ種別.背景動画 )
1076 App.サウンドタイマ.一時停止する();
1079 this._背景動画?.再生を開始する();
1080 this._背景動画開始済み = true;
1084 this._BGM再生開始済み = true;
1090 if( App.システム設定.Autoチップのドラム音を再生する )
1091 this._ドラムサウンド.発声する( chip.チップ種別, ( chip.音量 / (float) チップ.最大音量 ) );
1098 #region " チップの判定処理を行う。"
1100 if( hitRankType != ヒットランク種別.MISS )
1104 var 対応表 = App.ユーザ管理.選択されているユーザ.オプション設定.ドラムとチップと入力の対応表[ chip.チップ種別 ];
1110 レーンフレームの左端位置 + レーンフレーム.レーンto横中央相対位置[ 対応表.表示レーン種別 ],
1113 this._ドラムセット.ヒットアニメ開始( 対応表.ドラム入力種別, App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右 );
1115 this._ヒットランク.表示開始( chip.チップ種別, hitRankType );
1116 this.ヒットランク別ヒット回数[ hitRankType ]++;
1122 this._コンボ.COMBO値 = 0;
1124 this._ヒットランク.表示開始( chip.チップ種別, ヒットランク種別.MISS );
1125 this.ヒットランク別ヒット回数[ hitRankType ]++;
1132 #region " チップを非表示にする。"
1134 if( hitRankType != ヒットランク種別.MISS )
1136 chip.可視 = false; // PERFECT~POOR チップは非表示。
1141 chip.可視 = false; // MISSチップは最後まで表示し続ける。