2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Diagnostics;
6 using System.Threading;
7 using System.Threading.Tasks;
9 using SharpDX.DirectInput;
12 using FDK.メディア.サウンド.WASAPI;
19 using Utilities = FDK.Utilities;
40 public ConcurrentDictionary<ヒットランク種別, int> ヒットランク別ヒット回数
43 } = new ConcurrentDictionary<ヒットランク種別, int>();
48 this.子リスト.Add( this._ステージ台 = new 画像( @"$(System)\images\ステージ台.png" ) );
49 this.子リスト.Add( this._レーンフレーム = new レーンフレーム() );
50 this.子リスト.Add( this._ドラムセット = new ドラムセット() );
51 this.子リスト.Add( this._ヒット判定バー = new 画像( @"$(System)\images\判定バー.png" ) );
52 this.子リスト.Add( this._チップ画像 = new 画像( @"$(System)\images\Chips.png" ) );
53 this.子リスト.Add( this._回転羽 = new 回転羽( 最大同時発火数: 32 ) );
54 this.子リスト.Add( this._コンソールフォント = new コンソールフォント() );
55 this.子リスト.Add( this._ドラムサウンド = new ドラムサウンド() );
56 this.子リスト.Add( this._コンボ = new コンボ() );
57 this.子リスト.Add( this._ヒットランク = new ヒットランク() );
58 this.子リスト.Add( this._FPSパラメータ = new 文字列画像() );
61 protected override void On活性化( デバイスリソース dr )
63 using( Log.Block( Utilities.現在のメソッド名 ) )
67 this._活性化した直後である = true;
68 this._演奏開始時刻sec = 0.0;
69 this._背景動画開始済み = false;
70 this._現在進行描画中の譜面スクロール速度の倍率 = App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率;
74 //this._デコード済みWaveSource = null; --> キャッシュなので消さない。
75 this._背景動画開始済み = false;
76 this._BGM再生開始済み = false;
77 this._高頻度進行タスクへのイベント = new TriStateEvent( TriStateEvent.状態種別.OFF );
78 this._チップ画像の矩形リスト = new 矩形リスト( @"$(System)\images\Chips Rectangle List.xml" ); // デバイスリソースは持たないので、子Activityではない。
80 this.ヒットランク別ヒット回数.Clear();
81 foreach( var hitRankType in typeof( ヒットランク種別 ).GetEnumValues() )
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.ビュアーモードではない )
120 this.現在のフェーズ = フェーズ.演奏中;
124 this.現在のフェーズ = フェーズ.ビュアーメッセージ待機;
132 protected override void On非活性化( デバイスリソース dr )
134 using( Log.Block( Utilities.現在のメソッド名 ) )
136 lock( this._スレッド間同期 )
138 #region " 高頻度進行タスクがまだ実行されていれば、終了する。 "
140 if( this._高頻度進行タスクへのイベント.現在の状態 == TriStateEvent.状態種別.ON )
143 this._高頻度進行タスクへのイベント.現在の状態 = TriStateEvent.状態種別.OFF;
144 //this._高頻度進行タスクへのイベント.無効になるまでブロックする(); --> タスクが終了するまで待つ必要はない。
149 // 背景動画を生成した場合は子リストから削除。
150 if( null != this._背景動画 )
151 this.子リスト.Remove( this._背景動画 );
153 this._活性化した直後である = false;
158 public override void 進行描画する( デバイスリソース dr )
160 Debug.Assert( this.活性化している );
161 Debug.Assert( null != dr );
163 lock( this._スレッド間同期 )
165 double 演奏時刻sec = this._演奏開始からの経過時間secを返す();
167 switch( this.現在のフェーズ )
172 if( this._活性化した直後である )
174 this._活性化した直後である = false;
175 this._描画開始チップ番号 = 0; // -1 → 0; 演奏開始。
176 this._チップアニメ.開始する( 最初の値: 0, 最後の値: 48, 値をひとつ増加させるのにかける時間ms: 10 );
178 #region " 演奏開始時刻を初期化し、動画とBGMの再生を開始する。"
180 double 演奏開始位置の先頭からの時間sec = 0.0;
183 this._演奏開始時刻sec = App.サウンドデバイス.GetDevicePosition() - 演奏開始位置の先頭からの時間sec;
184 Log.Info( $"演奏開始時刻(背景動画再生チェック前): {this._演奏開始時刻sec} sec" );
186 // 演奏開始時刻の設定(2) ビュアーメッセージがある場合、開始時刻を修正する。
187 var msg = App.最後に取得したビュアーメッセージ;
191 演奏開始位置の先頭からの時間sec = this._演奏開始小節番号を設定しその時刻secを返す( msg.演奏を開始する小節番号 );
192 Log.Info( $"演奏開始の先頭からの時間: {演奏開始位置の先頭からの時間sec} sec" );
195 App.システム設定.Autoチップのドラム音を再生する = msg.ドラムチップのヒット時に発声する;
198 // BGMと動画を(必要あれば)再生開始。
199 this._再生中の時刻なら動画とBGMを再生開始する( 演奏開始位置の先頭からの時間sec );
201 // 演奏開始時刻の設定(3) 動画とBGMが再生された場合、ここに到達するまでに多少の遅延が生じているので、ここで演奏開始時刻を再取得しておく。
202 this._演奏開始時刻sec = App.サウンドデバイス.GetDevicePosition() - 演奏開始位置の先頭からの時間sec;
203 Log.Info( $"演奏開始時刻(背景動画再生チェック後): {this._演奏開始時刻sec} sec" );
207 this._FPS = new FPS();
210 Task.Factory.StartNew( this._高頻度進行処理タスクエントリ );
214 #region " 譜面スクロール速度が変化している → 追い付き進行 "
217 double 倍率 = this._現在進行描画中の譜面スクロール速度の倍率;
219 if( 倍率 < App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 )
221 // todo: 時間間隔に関係なく進行描画ごとに倍率を変えてしまっているので、あとからVPSに依存しないよう修正すること。
222 this._現在進行描画中の譜面スクロール速度の倍率 =
223 Math.Min( 倍率 + 0.05, App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 );
225 else if( 倍率 > App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 )
228 this._現在進行描画中の譜面スクロール速度の倍率 =
229 Math.Max( 倍率 - 0.05, App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 );
234 #region " 背景動画とBGMの進行描画。"
238 // 背景動画チップがヒット済みなら、背景動画の進行描画を行う。
239 switch( App.ユーザ管理.選択されているユーザ.オプション設定.動画表示パターン種別 )
241 case 動画表示パターン種別.中央表示:
245 float w = dr.設計画面サイズdpx.Width;
246 float h = dr.設計画面サイズdpx.Height;
248 this._背景動画?.進行描画する( dr, new RectangleF( 0f, 0f, w, h ), 0.2f ); // 全体
251 float 上移動dpx = 100.0f;
253 // 進行描画せず、直前に取得したフレームをそのまま描画する。
254 this._背景動画?.前のフレームを描画する( dr, new RectangleF(
255 w * ( 1f - 拡大縮小率 ) / 2f,
256 h * ( 1f - 拡大縮小率 ) / 2f - 上移動dpx,
264 case 動画表示パターン種別.最大表示:
267 this._背景動画?.進行描画する( dr, new RectangleF( 0f, 0f, dr.設計画面サイズdpx.Width, dr.設計画面サイズdpx.Height ), 1.0f );
273 // 動画が重たいかもしれないので、演奏時刻をここで更新しておく。
274 演奏時刻sec = this._演奏開始からの経過時間secを返す();
276 // 背景動画が再生されているのにBGMがまだ再生されていないなら、すぐに再生を開始する。
277 if( false == this._BGM再生開始済み )
280 this._BGM再生開始済み = true;
285 this._ステージ台.描画する( dr, 0f, 0f );
286 this._レーンフレーム.進行描画する( dr, レーンフレームの左端位置dpx );
287 this._コンボ.進行描画する( dr );
288 this._ヒットランク.進行描画する( dr, レーンフレームの左端位置dpx );
289 this._小節線拍線を描画する( dr, 演奏時刻sec );
290 this._ヒット判定バー.描画する( dr, 597f, ヒット判定バーの中央Y座標dpx - 43f );
291 this._ドラムセット.進行描画する( dr );
292 this._チップを描画する( dr, 演奏時刻sec );
293 this._回転羽.進行描画する( dr );
294 this._FPS.VPSをカウントする();
295 this._FPSパラメータ.表示文字列 = $"VPS: {this._FPS.現在のVPS.ToString()} / FPS: {this._FPS.現在のFPS.ToString()}";
296 this._FPSパラメータ.進行描画する( dr, 0f, 0f );
308 case フェーズ.ビュアーメッセージ待機:
314 public void 演奏を停止する()
316 using( Log.Block( Utilities.現在のメソッド名 ) )
318 lock( this._スレッド間同期 )
320 this._描画開始チップ番号 = -1; // 演奏停止
323 this._背景動画開始済み = false;
325 this._コンボ.COMBO値 = 0;
331 /// 演奏クリア時には、次の結果ステージに入ってもBGMが鳴り続ける。
332 /// そのため、後からBGMだけを別個に停止するためのメソッドが必要になる。
334 public void BGMを停止する()
336 using( Log.Block( Utilities.現在のメソッド名 ) )
338 lock( this._スレッド間同期 )
341 this._BGM?.Dispose();
344 //this._デコード済みWaveSource?.Dispose(); --> ここではまだ解放しない。
345 //this._デコード済みWaveSource = null;
350 public void BGMのキャッシュを解放する()
352 using( Log.Block( Utilities.現在のメソッド名 ) )
355 Utilities.解放する( ref this._デコード済みWaveSource );
360 private const float ヒット判定バーの中央Y座標dpx = 869f + 43f;
362 private const float レーンフレームの左端位置dpx = 619f;
365 private double _演奏開始時刻sec = 0.0;
367 private double _現在進行描画中の譜面スクロール速度の倍率 = 1.0;
370 /// 演奏スコア.チップリスト[] のうち、描画を始めるチップのインデックス番号。
374 /// 演奏開始直後は 0 で始まり、対象番号のチップが描画範囲を流れ去るたびに +1 される。
375 /// このメンバの更新は、高頻度進行タスクではなく、進行描画メソッドで行う。(低精度で構わないので)
377 private int _描画開始チップ番号 = -1;
379 private 動画 _背景動画 = null;
382 /// 停止と解放は、演奏ステージクラスの非活性化後に、外部から行われる。
383 /// <see cref="SST.ステージ.演奏.演奏ステージ.BGMを停止する"/>
384 /// <see cref="SST.ステージ.演奏.演奏ステージ.BGMのキャッシュを解放する"/>
386 private Sound _BGM = null;
389 /// BGM の生成もとになるデコード済みサウンドデータ。
392 /// 活性化と非活性化に関係なく、常に最後にデコードしたデータを持つ。(キャッシュ)
393 /// 演奏ステージインスタンスを破棄する際に、このインスタンスもDisposeすること。
395 private DecodedWaveSource _デコード済みWaveSource = null;
397 private bool _背景動画開始済み = false;
399 private bool _BGM再生開始済み = false;
401 private 画像 _ステージ台 = null;
403 private レーンフレーム _レーンフレーム = null;
405 private ドラムセット _ドラムセット = null;
407 private 画像 _ヒット判定バー = null;
409 private 画像 _チップ画像 = null;
411 private 矩形リスト _チップ画像の矩形リスト = null;
413 private 単純増加後反復カウンタ _チップアニメ = new 単純増加後反復カウンタ();
415 private 回転羽 _回転羽 = null;
417 private コンソールフォント _コンソールフォント = null;
419 private ドラムサウンド _ドラムサウンド = null;
421 private コンボ _コンボ = null;
423 private ヒットランク _ヒットランク = null;
425 private FPS _FPS = null;
427 private 文字列画像 _FPSパラメータ = null;
429 private bool _活性化した直後である;
432 /// OFF:タスク未生成、ON:タスク稼働中、無効:タスク終了済み
434 private TriStateEvent _高頻度進行タスクへのイベント = null;
436 private readonly object _スレッド間同期 = new object();
442 /// 指定した小節の先頭を演奏開始位置として設定し、その位置(時刻)を返す。
444 /// <returns>演奏開始位置sec。(0~)</returns>
445 private double _演奏開始小節番号を設定しその時刻secを返す( int 演奏開始小節番号 )
447 lock( this._スレッド間同期 )
449 var score = App.演奏スコア;
454 double 演奏開始時刻sec = 0.0;
456 for( int i = 0; i < score.チップリスト.Count; i++ )
458 if( score.チップリスト[ i ].小節番号 < 演奏開始小節番号 )
460 // 開始チップ以前のチップはヒット済みとする。
461 score.チップリスト[ i ].ヒット済みの状態にする();
468 // 演奏開始時刻は、開始チップの発声時刻から少し早めの値に。
469 演奏開始時刻sec = score.チップリスト[ i ].発声時刻sec;
472 // 開始チップ以降のすべてのチップをヒット前の状態にする。
473 for( int j = i; j >= 0; j-- )
475 if( score.チップリスト[ j ].ヒット済みである )
476 score.チップリスト[ j ].ヒット前の状態にする();
487 private void _再生中の時刻なら動画とBGMを再生開始する( double 時刻sec )
489 using( Log.Block( Utilities.現在のメソッド名 ) )
491 lock( this._スレッド間同期 )
493 Log.Info( $"指定された時刻: {時刻sec} sec" );
495 var 背景動画チップ = App.演奏スコア.チップリスト.FirstOrDefault( (chip) => ( chip.チップ種別 == チップ種別.背景動画 ) );
497 if( null == 背景動画チップ )
499 Log.Info( "背景動画チップは存在しません。" );
503 double 背景動画の長さsec = this._BGM?.長さsec ?? 0.0; // 動画のサイズは、映像ではなく音声を優先する。
505 Log.Info( $"背景動画の発生時刻: {背景動画チップ.発声時刻sec} sec" );
506 Log.Info( $"背景動画の長さ: {背景動画の長さsec} sec" );
509 if( ( 背景動画チップ.発声時刻sec <= 時刻sec ) && ( 時刻sec < ( 背景動画チップ.発声時刻sec + 背景動画の長さsec ) ) )
512 double 再生開始時刻sec = ( 時刻sec - 背景動画チップ.発声時刻sec );
513 this._背景動画?.再生を開始する( 再生開始時刻sec );
514 this._背景動画開始済み = true;
515 this._BGM?.Play( 再生開始時刻sec );
516 this._BGM再生開始済み = true;
517 Log.Info( $"背景動画の再生を開始しました。(再生開始時刻: {再生開始時刻sec} sec)" );
521 Log.Info( $"指定された時刻は背景動画の再生期間内ではないので、何もしません。" );
527 private double _演奏開始からの経過時間secを返す()
529 lock( this._スレッド間同期 )
531 return App.サウンドデバイス.GetDevicePosition() - this._演奏開始時刻sec;
535 // 小節線・拍線 と チップ は描画階層(奥行き)が異なるので、別々のメソッドに分ける。
537 private void _小節線拍線を描画する( デバイスリソース dr, double 現在の演奏時刻sec )
539 if( null == this._チップ画像 )
542 this._チップ画像.加算合成 = false;
544 this._描画範囲のチップに処理を適用する( ( chip, index, ヒット判定バーとの時間sec, ヒット判定バーとの距離dpx ) => {
546 if( chip.チップ種別 == チップ種別.小節線 )
550 float 左位置dpx = レーンフレームの左端位置dpx + レーンフレーム.レーンto横中央相対位置dpx[ 表示レーン種別.LeftCrash ] - 1f; // -1f はレーン線の幅の半分。
551 float 上位置dpx = (float)( ヒット判定バーの中央Y座標dpx + ヒット判定バーとの距離dpx - 1f ); // -1f は小節線の厚みの半分。
552 if( this._チップ画像の矩形リスト[ nameof( チップ種別.小節線 ) ] is RectangleF 画像範囲 )
554 this._チップ画像.描画する( dr, 左位置dpx, 上位置dpx, 転送元矩形dpx: 画像範囲 );
555 this._チップ画像.描画する( dr, 左位置dpx + 画像範囲.Width, 上位置dpx, 転送元矩形dpx: 画像範囲 );
560 else if( chip.チップ種別 == チップ種別.拍線 )
564 float 左位置dpx = レーンフレームの左端位置dpx + レーンフレーム.レーンto横中央相対位置dpx [ 表示レーン種別.LeftCrash ] - 1f; // -1f はレーン線の幅の半分。
565 float 上位置dpx = (float)( ヒット判定バーの中央Y座標dpx + ヒット判定バーとの距離dpx - 1f ); // -1f は拍線の厚みの半分。
566 if( this._チップ画像の矩形リスト[ nameof( チップ種別.拍線 ) ] is RectangleF 画像範囲 )
568 this._チップ画像.描画する( dr, 左位置dpx: 左位置dpx, 上位置dpx: 上位置dpx, 転送元矩形dpx: 画像範囲 );
569 this._チップ画像.描画する( dr, 左位置dpx: 左位置dpx + 画像範囲.Width, 上位置dpx: 上位置dpx, 転送元矩形dpx: 画像範囲 );
578 private void _チップを描画する( デバイスリソース dr, double 現在の演奏時刻sec )
580 if( null == this._チップ画像 )
583 this._チップ画像.加算合成 = false;
585 this._描画範囲のチップに処理を適用する( ( chip, index, ヒット判定バーとの時間sec, ヒット判定バーとの距離dpx ) => {
587 float 縦中央位置dpx = (float)( ヒット判定バーの中央Y座標dpx + ヒット判定バーとの距離dpx );
589 #region " チップが描画開始チップであり、かつ、そのY座標が画面下端を超えたなら、描画開始チップ番号を更新する。"
591 if( ( index == this._描画開始チップ番号 ) &&
592 ( dr.設計画面サイズdpx.Height + 40.0 < 縦中央位置dpx ) ) // +40 はチップが隠れるであろう適当なマージン。
596 if( App.演奏スコア.チップリスト.Count <= this._描画開始チップ番号 )
598 this.現在のフェーズ = フェーズ.クリア;
599 this._描画開始チップ番号 = -1; // 演奏完了。
609 #region " チップを個別に描画する。"
611 float 音量0to1 = chip.音量 / (float) チップ.最大音量;
615 case チップ種別.LeftCrash:
616 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ nameof( チップ種別.LeftCrash ) ], 縦中央位置dpx, 音量0to1 );
619 case チップ種別.HiHat_Close:
620 _アニメチップを1つ描画する( 表示レーン種別.HiHat, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Close ) ], 縦中央位置dpx, 音量0to1 );
623 case チップ種別.HiHat_HalfOpen:
624 _アニメチップを1つ描画する( 表示レーン種別.HiHat, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Close ) ], 縦中央位置dpx, 音量0to1 );
625 _単画チップを1つ描画する( 表示レーン種別.Foot, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_HalfOpen ) ], 縦中央位置dpx, 1.0f );
628 case チップ種別.HiHat_Open:
629 _アニメチップを1つ描画する( 表示レーン種別.HiHat, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Close ) ], 縦中央位置dpx, 音量0to1 );
630 _単画チップを1つ描画する( 表示レーン種別.Foot, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Open ) ], 縦中央位置dpx, 1.0f );
633 case チップ種別.HiHat_Foot:
634 _単画チップを1つ描画する( 表示レーン種別.Foot, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Foot ) ], 縦中央位置dpx, 1.0f );
638 _アニメチップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare ) ], 縦中央位置dpx, 音量0to1 );
641 case チップ種別.Snare_ClosedRim:
642 _単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare_ClosedRim ) ], 縦中央位置dpx, 1.0f );
645 case チップ種別.Snare_OpenRim:
646 _単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare_OpenRim ) ], 縦中央位置dpx, 音量0to1 );
648 //_単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare ) ], 縦中央位置dpx, 音量0to1 );
651 case チップ種別.Snare_Ghost:
652 _単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare_Ghost ) ], 縦中央位置dpx, 1.0f );
656 _アニメチップを1つ描画する( 表示レーン種別.Bass, this._チップ画像の矩形リスト[ nameof( チップ種別.Bass ) ], 縦中央位置dpx, 音量0to1 );
660 _アニメチップを1つ描画する( 表示レーン種別.Tom1, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom1 ) ], 縦中央位置dpx, 音量0to1 );
664 _単画チップを1つ描画する( 表示レーン種別.Tom1, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom1_Rim ) ], 縦中央位置dpx, 1.0f );
668 _アニメチップを1つ描画する( 表示レーン種別.Tom2, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom2 ) ], 縦中央位置dpx, 音量0to1 );
672 _単画チップを1つ描画する( 表示レーン種別.Tom2, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom2_Rim ) ], 縦中央位置dpx, 1.0f );
676 _アニメチップを1つ描画する( 表示レーン種別.Tom3, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom3 ) ], 縦中央位置dpx, 音量0to1 );
680 _単画チップを1つ描画する( 表示レーン種別.Tom3, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom3_Rim ) ], 縦中央位置dpx, 1.0f );
683 case チップ種別.RightCrash:
684 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ nameof( チップ種別.RightCrash ) ], 縦中央位置dpx, 音量0to1 );
688 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Chinaは左 )
690 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftChina" ], 縦中央位置dpx, 音量0to1 );
694 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightChina" ], 縦中央位置dpx, 音量0to1 );
699 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Rideは左 )
701 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftRide" ], 縦中央位置dpx, 音量0to1 );
705 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightRide" ], 縦中央位置dpx, 音量0to1 );
710 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Rideは左 )
712 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftRide_Cup" ], 縦中央位置dpx, 音量0to1 );
716 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightRide_Cup" ], 縦中央位置dpx, 音量0to1 );
721 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Splashは左 )
723 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftSplash" ], 縦中央位置dpx, 音量0to1 );
727 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightSplash" ], 縦中央位置dpx, 音量0to1 );
731 case チップ種別.LeftCymbal_Mute:
732 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftCymbal_Mute" ], 縦中央位置dpx, 1.0f );
735 case チップ種別.RightCymbal_Mute:
736 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightCymbal_Mute" ], 縦中央位置dpx, 1.0f );
746 void _単画チップを1つ描画する( 表示レーン種別 lane, RectangleF? 元矩形dpx, float 上位置dpx, float 音量0to1 )
748 lock( this._スレッド間同期 )
753 var 画像範囲dpx = (RectangleF) 元矩形dpx;
757 左位置dpx: レーンフレームの左端位置dpx + レーンフレーム.レーンto横中央相対位置dpx[ lane ] - ( 画像範囲dpx.Width / 2f ),
758 上位置dpx: 上位置dpx - ( ( 画像範囲dpx.Height / 2f ) * 音量0to1 ),
763 void _アニメチップを1つ描画する( 表示レーン種別 lane, RectangleF? 画像範囲orNull, float Ydpx, float 音量0to1 )
765 lock( this._スレッド間同期 )
767 if( null == 画像範囲orNull )
770 var 画像範囲 = (RectangleF) 画像範囲orNull;
772 float チップ1枚の高さdpx = 18f;
773 画像範囲.Offset( 0f, this._チップアニメ.現在値 * 15f ); // 下端3dpxは下のチップと共有する前提のデザインなので、18f-3f = 15f。
774 画像範囲.Height = チップ1枚の高さdpx;
775 float 左位置dpx = レーンフレームの左端位置dpx + レーンフレーム.レーンto横中央相対位置dpx[ lane ] - ( 画像範囲.Width / 2f );
776 float 上位置dpx = Ydpx - ( チップ1枚の高さdpx / 2f ) * 音量0to1;
778 this._チップ画像?.描画する( dr, 左位置dpx, 上位置dpx, 転送元矩形dpx: 画像範囲, Y方向拡大率: 音量0to1 );
791 /// 描画処理とは独立したタスクを使い、より高頻度にループさせる。
793 private void _高頻度進行処理タスクエントリ()
795 Log.Info( "高頻度進行処理タスクを開始します。" );
797 this._高頻度進行タスクへのイベント.現在の状態 = TriStateEvent.状態種別.ON;
799 while( this._高頻度進行タスクへのイベント.現在の状態 == TriStateEvent.状態種別.ON )
801 lock( this._スレッド間同期 )
805 this._FPS.FPSをカウントしプロパティを更新する();
809 this._描画範囲のチップに処理を適用する( ( chip, index, ヒット判定バーとの時間sec, ヒット判定バーとの距離dpx ) => {
811 var オプション設定 = App.ユーザ管理.選択されているユーザ.オプション設定;
812 var 対応表 = オプション設定.ドラムとチップと入力の対応表.対応表[ chip.チップ種別 ];
813 var AutoPlay = オプション設定.AutoPlay[ 対応表.AutoPlay種別 ];
815 bool チップはヒット済みである = chip.ヒット済みである;
816 bool チップはMISSエリアに達している = ( ヒット判定バーとの時間sec > オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] );
817 bool チップはヒット判定バーを通過した = ( 0 <= ヒット判定バーとの距離dpx );
825 if( チップはMISSエリアに達している )
828 if( AutoPlay && 対応表.AutoPlayON.MISS判定 )
830 this._チップのヒット処理を行う( chip, ヒットランク種別.MISS, 対応表.AutoPlayON.自動ヒット時処理 );
833 else if( !AutoPlay && 対応表.AutoPlayOFF.MISS判定 )
835 this._チップのヒット処理を行う( chip, ヒットランク種別.MISS, 対応表.AutoPlayOFF.ユーザヒット時処理 );
844 if( チップはヒット判定バーを通過した )
847 if( AutoPlay && 対応表.AutoPlayON.自動ヒット )
849 this._チップのヒット処理を行う( chip, ヒットランク種別.AUTO, 対応表.AutoPlayON.自動ヒット時処理 );
852 else if( !AutoPlay && 対応表.AutoPlayOFF.自動ヒット )
854 this._チップのヒット処理を行う( chip, ヒットランク種別.AUTO, 対応表.AutoPlayOFF.自動ヒット時処理 );
869 App.入力管理.すべての入力デバイスをポーリングする( 入力履歴を記録する: false );
871 if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Escape ) )
873 #region " ESC → ステージキャンセル "
875 if( App.ビュアーモードではない )
878 this.現在のフェーズ = フェーズ.キャンセル;
883 // ビュアーモード時のキャンセルは無効。
888 if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Up ) )
890 #region " 上 → 譜面スクロールを加速 "
892 const double 最大倍率 = 8.0;
893 App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 =
894 Math.Min( App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 + 0.5, 最大倍率 );
898 if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Down ) )
900 #region " 下 → 譜面スクロールを減速 "
902 const double 最小倍率 = 0.5;
903 App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 =
904 Math.Max( App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 - 0.5, 最小倍率 );
911 foreach( var 入力 in App.入力管理.ポーリング結果 )
913 if( 入力.InputEvent.離された )
914 continue; // 押下イベントじゃないなら無視。
916 // todo: チップに対応するパッドのアニメを開始。
919 var 処理済み入力 = new List<ドラム入力イベント>(); // ヒット処理が終わった入力は、二重処理しないよう、この中に追加しておく。
921 this._描画範囲のチップに処理を適用する( ( chip, index, ヒット判定バーとの時間sec, ヒット判定バーとの距離dpx ) => {
923 var オプション設定 = App.ユーザ管理.選択されているユーザ.オプション設定;
924 var 対応表 = オプション設定.ドラムとチップと入力の対応表.対応表[ chip.チップ種別 ];
925 var AutoPlay = オプション設定.AutoPlay[ 対応表.AutoPlay種別 ];
926 var ヒット判定バーとの時間の絶対値sec = Math.Abs( ヒット判定バーとの時間sec );
928 bool チップはヒット済みである = chip.ヒット済みである;
929 bool チップはMISSエリアに達している = ( ヒット判定バーとの時間sec > オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] );
930 bool チップはヒット可能エリアにある = ( ヒット判定バーとの時間sec >= -( オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] ) && !チップはMISSエリアに達している );
932 if( AutoPlay || チップはヒット済みである || !( 対応表.AutoPlayOFF.ユーザヒット ) || !( チップはヒット可能エリアにある ) )
937 // todo: シンバルフリーを実装する。
938 var ヒット入力 = App.入力管理.ポーリング結果.FirstOrDefault( ( 入力 ) =>
939 ( 入力.InputEvent.押された ) &&
940 ( 対応表.ドラム入力種別 == 入力.Type ) &&
941 !( 処理済み入力.Contains( 入力 ) )
955 var ヒットランク = ヒットランク種別.POOR;
957 if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.PERFECT ] )
959 ヒットランク = ヒットランク種別.PERFECT;
961 else if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.GREAT ] )
963 ヒットランク = ヒットランク種別.GREAT;
965 else if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.GOOD ] )
967 ヒットランク = ヒットランク種別.GOOD;
972 this._チップのヒット処理を行う( chip, ヒットランク, 対応表.AutoPlayOFF.ユーザヒット時処理 );
983 this._高頻度進行タスクへのイベント.現在の状態 = TriStateEvent.状態種別.無効;
985 Log.Info( "高頻度進行処理タスクを終了しました。" );
989 /// <see cref="_描画開始チップ番号"/> から画面上端にはみ出すまでの間の各チップに対して、指定された処理を適用する。
991 /// <param name="適用する処理">引数は、順に、対象のチップ、チップ番号、ヒット判定バーとの時間sec、ヒット判定バーとの距離dpx</param>
992 private void _描画範囲のチップに処理を適用する( Action<チップ, int, double, double> 適用する処理 )
994 lock( this._スレッド間同期 )
1000 double リアルタイム演奏時刻sec = this._演奏開始からの経過時間secを返す();
1002 for( int i = this._描画開始チップ番号; ( 0 <= i ) && ( i < スコア.チップリスト.Count ); i++ )
1004 var チップ = スコア.チップリスト[ i ];
1006 // ヒット判定バーとチップの間の、時間 と 距離 を算出。→ いずれも、負数ならバー未達、0でバー直上、正数でバー通過。
1007 double ヒット判定バーとの時間sec = リアルタイム演奏時刻sec - チップ.描画時刻sec;
1008 double ヒット判定バーとの距離dpx = スコア.指定された時間secに対応する符号付きピクセル数を返す( this._現在進行描画中の譜面スクロール速度の倍率, ヒット判定バーとの時間sec );
1011 bool チップは画面上端より上に出ている = ( ( ヒット判定バーの中央Y座標dpx + ヒット判定バーとの距離dpx ) < -40.0 ); // -40dpx はチップが隠れるであろう適当なマージン。
1012 if( チップは画面上端より上に出ている )
1015 // 処理実行。開始判定(描画開始チップ番号の更新)もこの中で。
1016 適用する処理( チップ, i, ヒット判定バーとの時間sec, ヒット判定バーとの距離dpx );
1021 private void _チップのヒット処理を行う( チップ chip, ヒットランク種別 hitRankType, ドラムとチップと入力の対応表.Column.Columnヒット処理 ヒット処理表 )
1023 lock( this._スレッド間同期 )
1025 chip.ヒット済みである = true;
1029 #region " チップを再生する。"
1031 if( chip.チップ種別 == チップ種別.背景動画 )
1034 this._背景動画?.再生を開始する();
1035 this._背景動画開始済み = true;
1039 this._BGM再生開始済み = true;
1043 if( App.システム設定.Autoチップのドラム音を再生する )
1044 this._ドラムサウンド.発声する( chip.チップ種別, ( chip.音量 / ( float ) チップ.最大音量 ) );
1051 #region " チップの判定処理を行う。"
1053 if( hitRankType != ヒットランク種別.MISS )
1058 レーンフレームの左端位置dpx + レーンフレーム.レーンto横中央相対位置dpx[ App.ユーザ管理.選択されているユーザ.オプション設定.ドラムとチップと入力の対応表.対応表[ chip.チップ種別 ].表示レーン種別 ],
1059 ヒット判定バーの中央Y座標dpx ) );
1060 this._ヒットランク.表示開始( chip.チップ種別, hitRankType );
1061 this.ヒットランク別ヒット回数[ hitRankType ]++;
1065 this._コンボ.COMBO値 = 0;
1066 this._ヒットランク.表示開始( chip.チップ種別, ヒットランク種別.MISS );
1067 this.ヒットランク別ヒット回数[ hitRankType ]++;
1074 #region " チップを非表示にする。"
1076 if( hitRankType != ヒットランク種別.MISS )
1078 chip.可視 = false; // PERFECT~POOR チップは非表示。
1083 chip.可視 = false; // MISSチップは最後まで表示し続ける。