--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using SSTFormat.v2;
+
+namespace SST.ステージ.演奏
+{
+ /// <summary>
+ /// SSTFormat.v2.チップ種別/チップ の拡張メソッド
+ /// </summary>
+ static class チップExtensions
+ {
+ /// <summary>
+ /// チップの排他発声グループID を表す。
+ /// ただし、グループID 0 は排他発声にはしないことを意味する。
+ /// </summary>
+ /// <remarks>
+ /// このグループIDを 0 以外に設定すると、チップの発声時に、そのチップ種別に対応する排他発声グループIDを参照して
+ /// それと同じ値を持つチップ種別のサウンドがすべて再生停止する必要があるかを確認し、必要あれば停止してから発声されるようなる。
+ /// なお、同一かどうかだけを見るので、グループIDに設定する数値は(0を除いて)どんな値でもいい。
+ /// </remarks>
+ /// <param name="チップ種別">調べるチップ種別。</param>
+ /// <returns>チップ種別に対応する排他発声グループID。</returns>
+ public static int 排他発声グループID( this チップ種別 チップ種別 )
+ {
+ var user = App.ユーザ管理.選択されているユーザ;
+
+ switch( チップ種別 )
+ {
+ case チップ種別.HiHat_Close:
+ case チップ種別.HiHat_Foot:
+ case チップ種別.HiHat_HalfOpen:
+ case チップ種別.HiHat_Open:
+ return GID_HIHAT;
+
+ case チップ種別.LeftCrash:
+ case チップ種別.LeftCymbal_Mute:
+ return GID_LEFT_CYMBAL;
+
+ case チップ種別.RightCrash:
+ case チップ種別.RightCymbal_Mute:
+ return GID_RIGHT_CYMBAL;
+
+ case チップ種別.China:
+ return ( user.オプション設定.表示レーンの左右.Chinaは左 ) ? GID_LEFT_CYMBAL : GID_RIGHT_CYMBAL;
+
+ case チップ種別.Splash:
+ return ( user.オプション設定.表示レーンの左右.Splashは左 ) ? GID_LEFT_CYMBAL : GID_RIGHT_CYMBAL;
+
+ case チップ種別.Ride:
+ case チップ種別.Ride_Cup:
+ return ( user.オプション設定.表示レーンの左右.Rideは左 ) ? GID_LEFT_CYMBAL : GID_RIGHT_CYMBAL;
+ }
+
+ return 0;
+ }
+ public static int 排他発声グループID( this チップ チップ )
+ {
+ return チップ.チップ種別.排他発声グループID();
+ }
+
+ public static bool 直前のチップを消音する( this チップ種別 今回のチップの種別, チップ種別 直前のチップの種別 )
+ {
+ int 今回のチップのGID = 今回のチップの種別.排他発声グループID();
+ int 直前のチップのGID = 直前のチップの種別.排他発声グループID();
+
+ if( 直前のチップのGID != 今回のチップのGID )
+ return false;
+
+ // グループIDがシンバルである場合は、Mute が来た場合を除き、消音しない。
+
+ if( 直前のチップのGID == GID_LEFT_CYMBAL )
+ {
+ return ( 今回のチップの種別 == チップ種別.LeftCymbal_Mute );
+ }
+ if( 直前のチップのGID == GID_RIGHT_CYMBAL )
+ {
+ return ( 今回のチップの種別 == チップ種別.RightCymbal_Mute );
+ }
+
+ return true;
+ }
+
+ private const int GID_HIHAT = 1;
+ private const int GID_LEFT_CYMBAL = 2;
+ private const int GID_RIGHT_CYMBAL = 3;
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using FDK;
+using FDK.メディア;
+using FDK.メディア.サウンド.WASAPI;
+using SSTFormat.v2;
+
+namespace SST.ステージ.演奏
+{
+ /// <summary>
+ /// AutoPlay用のドラムサウンド。
+ /// </summary>
+ class ドラムサウンド : Activity
+ {
+ public ドラムサウンド()
+ {
+ }
+
+ protected override void On活性化( デバイスリソース dr )
+ {
+ this._KitXmlを読み込む();
+ }
+
+ protected override void On非活性化( デバイスリソース dr )
+ {
+ this._すべてのコンテキストを初期化する();
+ }
+
+ public void 発声する( チップ種別 chipType, float 音量0to1 )
+ {
+ lock( this._スレッド間同期 )
+ {
+ if( false == this._チップtoコンテキスト.ContainsKey( chipType ) )
+ return; // コンテキスト未登録のチップなら何もしない。
+
+ var context = this._チップtoコンテキスト[ chipType ];
+
+ // 現在発声中のサウンドを全部止めるチップ種別の場合は止める。
+ if( 0 != chipType.排他発声グループID() ) // ID = 0 は対象外。
+ {
+ // 消音対象のコンテキストの Sounds[] を select する。
+ var 停止するサウンド群 =
+ from kvp in this._チップtoコンテキスト
+ where ( chipType.直前のチップを消音する( kvp.Key ) )
+ select kvp.Value.Sounds;
+
+ // 集めた Sounds[] をすべて停止する。
+ foreach( var sounds in 停止するサウンド群 )
+ {
+ foreach( var sound in sounds )
+ {
+ sound.Stop();
+ }
+ }
+ }
+
+ // 再生する。
+ if( null != context.Sounds[ context.次に再生するSound番号 ] )
+ {
+ context.Sounds[ context.次に再生するSound番号 ].Volume = 音量0to1;
+ context.Sounds[ context.次に再生するSound番号 ].Play();
+ }
+
+ // サウンドローテーション。
+ context.次に再生するSound番号++;
+
+ if( context.次に再生するSound番号 >= ドラムサウンド._多重度 )
+ context.次に再生するSound番号 = 0;
+ }
+ }
+
+
+ private const int _多重度 = 2;
+
+ private class Cコンテキスト : IDisposable
+ {
+ public Sound[] Sounds = new Sound[ _多重度 ];
+ public int 次に再生するSound番号 = 0;
+ public void Dispose()
+ {
+ if( null != this.Sounds )
+ {
+ for( int i = 0; i < this.Sounds.Length; i++ )
+ {
+ this.Sounds[ i ].Stop();
+ Utilities.解放する( ref this.Sounds[ i ] );
+ }
+ }
+ }
+ };
+
+ private Dictionary<チップ種別, Cコンテキスト> _チップtoコンテキスト = null;
+
+ private readonly string _KitXmlファイルパス = @"$(System)\sounds\Kit.xml";
+
+ private readonly object _スレッド間同期 = new object();
+
+
+ private void _すべてのコンテキストを初期化する()
+ {
+ lock( this._スレッド間同期 )
+ {
+ // コンテキストがすでに存在しているなら解放する。
+ if( null != this._チップtoコンテキスト )
+ {
+ foreach( var kvp in this._チップtoコンテキスト )
+ kvp.Value.Dispose();
+ }
+
+ // 生成する。
+ this._チップtoコンテキスト = new Dictionary<チップ種別, Cコンテキスト>();
+ }
+ }
+
+ private void _KitXmlを読み込む()
+ {
+ lock( this._スレッド間同期 )
+ {
+ this._すべてのコンテキストを初期化する();
+
+ string xmlPath = フォルダ.絶対パスに含まれるフォルダ変数を展開して返す( this._KitXmlファイルパス );
+
+ if( false == File.Exists( xmlPath ) )
+ {
+ Log.WARNING( $"Kit ファイルが存在しません。ドラムサウンドは生成されません。[{this._KitXmlファイルパス}]" );
+ return;
+ }
+
+ try
+ {
+ var xml文書 = XDocument.Load( xmlPath );
+
+ // <Root>
+ var Root要素 = xml文書.Element( "Root" );
+ {
+ // <DrumKit>
+ var DrumKit要素 = Root要素.Element( "DrumKit" ); // 最初の1つのみサポート。
+ var DrumKit要素のVersion属性 = DrumKit要素.Attribute( "Version" );
+ var DrumKit要素のName属性 = DrumKit要素.Attribute( "Name" ); // 現在は未使用。
+ if( "1.2" == DrumKit要素のVersion属性.Value )
+ {
+ #region " DrumKit ver1.2 "
+ //----------------
+ foreach( var DrumKitの子要素 in DrumKit要素.Elements() )
+ {
+ switch( DrumKitの子要素.Name.LocalName )
+ {
+ // <Sound>
+ case "Sound":
+ // Name="..." 属性の値は、SSTFormat.チップ種別 の各メンバの名前に等しいものとする。
+ var Sound要素のName属性 = DrumKitの子要素.Attribute( "Name" );
+ if( Enum.TryParse( Sound要素のName属性.Value, out チップ種別 chipType ) )
+ {
+ string サウンドファイルパス = Path.Combine( SST.IO.Folder.System, @"sounds\", DrumKitの子要素.Value );
+
+ if( File.Exists( サウンドファイルパス ) )
+ {
+ // すでに辞書に存在してるなら、解放して削除する。
+ if( this._チップtoコンテキスト.ContainsKey( chipType ) )
+ {
+ this._チップtoコンテキスト[ chipType ]?.Dispose();
+ this._チップtoコンテキスト.Remove( chipType );
+ }
+
+ // コンテキストを作成する。
+ var context = new Cコンテキスト() {
+ Sounds = new Sound[ ドラムサウンド._多重度 ],
+ 次に再生するSound番号 = 0,
+ };
+
+ // 多重度分のサウンドを生成する。
+ for( int i = 0; i < context.Sounds.Length; i++ )
+ context.Sounds[ i ] = App.サウンドデバイス.サウンドを生成する( サウンドファイルパス );
+
+ // コンテキストを辞書に追加する。
+ this._チップtoコンテキスト.Add( chipType, context );
+ }
+ else
+ {
+ Log.WARNING( $"サウンドファイル {フォルダ.絶対パスをフォルダ変数付き絶対パスに変換して返す( サウンドファイルパス )} が存在しません。[{this._KitXmlファイルパス}]" );
+ }
+ }
+ else
+ {
+ Log.WARNING( $"未知の要素 {Sound要素のName属性.Value} をスキップします。[{this._KitXmlファイルパス}]" );
+ }
+ break;
+ }
+ }
+ //----------------
+ #endregion
+ }
+ }
+ }
+ catch( Exception e )
+ {
+ Log.ERROR( $"Kitファイルの読み込みに失敗しました。{e.Message}[{this._KitXmlファイルパス}]" );
+ }
+ }
+ }
+ }
+}
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using SharpDX;
+using SharpDX.DirectInput;
using FDK;
using FDK.メディア;
using FDK.メディア.サウンド.WASAPI;
using FDK.カウンタ;
+using FDK.同期;
using SSTFormat.v2;
-using SST.曲;
+using SST.入力;
+using SST.設定;
using Utilities = FDK.Utilities;
set;
}
+
public 演奏ステージ()
{
this.子リスト.Add( this._ステージ台 = new 画像( @"$(System)\images\ステージ台.png" ) );
this.子リスト.Add( this._レーンフレーム = new レーンフレーム() );
this.子リスト.Add( this._ドラムセット = new ドラムセット() );
this.子リスト.Add( this._ヒット判定バー = new 画像( @"$(System)\images\判定バー.png" ) );
+ this.子リスト.Add( this._チップ画像 = new 画像( @"$(System)\images\Chips.png" ) );
+ this.子リスト.Add( this._コンソールフォント = new コンソールフォント() );
+ this.子リスト.Add( this._ドラムサウンド = new ドラムサウンド() );
}
protected override void On活性化( デバイスリソース dr )
this._活性化した直後である = true;
this._演奏開始時刻sec = 0.0;
this._背景動画開始済み = false;
- this._Autoチップのドラム音を再生する = true;
+ this._現在進行描画中の譜面スクロール速度の倍率 = App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率;
this._描画開始チップ番号 = -1;
this._背景動画 = null;
this._BGM = null;
- this._デコード済みWaveSource = null;
+ //this._デコード済みWaveSource = null; --> キャッシュなので消さない。
this._背景動画開始済み = false;
this._BGM再生開始済み = false;
+ this._高頻度進行タスクへのイベント = new TriStateEvent( TriStateEvent.状態種別.OFF );
+ this._チップ画像の矩形リスト = new 矩形リスト( @"$(System)\images\Chips Rectangle List.xml" ); // デバイスリソースは持たないので、子Activityではない。
+
+ #region " 背景動画とBGMを生成する。"
+ //----------------
+ if( ( null != App.演奏スコア ) && ( App.演奏スコア.背景動画ファイル名.Nullでも空でもない() ) )
+ {
+ Log.Info( "背景動画とBGMを読み込みます。" );
+
+ // 動画を子リストに追加。
+ this.子リスト.Add( this._背景動画 = new 動画( App.演奏スコア.背景動画ファイル名, App.システム設定.動画デコーダのキューサイズ ) );
+
+ // 動画から音声パートを抽出して BGM を作成。
+ if( ( null != this._デコード済みWaveSource ) && this._デコード済みWaveSource.Path.Equals( App.演奏スコア.背景動画ファイル名 ) )
+ {
+ // (A) 前回生成したBGMとパスが同じなので、前回のデコード済み WaveSource をキャッシュとして再利用する。
+ Log.Info( "前回生成したサウンドデータを再利用します。" );
+ }
+ else
+ {
+ // (B) 初めての生成か、または前回生成したBGMとパスが違うので、新しくデコード済み WaveSource を生成する。
+ this._デコード済みWaveSource?.Dispose();
+ this._デコード済みWaveSource = new DecodedWaveSource( App.演奏スコア.背景動画ファイル名, App.サウンドデバイス.WaveFormat );
+ }
+
+ this._BGM?.Dispose();
+ this._BGM = App.サウンドデバイス.サウンドを生成する( this._デコード済みWaveSource );
+ }
+ else
+ {
+ Log.Info( "背景動画とBGMはありません。" );
+ }
+ //----------------
+ #endregion
#region " 最初のフェーズを設定する。"
//----------------
{
lock( this._スレッド間同期 )
{
+ #region " 高頻度進行タスクがまだ実行されていれば、終了する。 "
+ //----------------
+ if( this._高頻度進行タスクへのイベント.現在の状態 == TriStateEvent.状態種別.ON )
+ {
+ // タスクへ終了を指示。
+ this._高頻度進行タスクへのイベント.現在の状態 = TriStateEvent.状態種別.OFF;
+ //this._高頻度進行タスクへのイベント.無効になるまでブロックする(); --> タスクが終了するまで待つ必要はない。
+ }
+ //----------------
+ #endregion
+
this._活性化した直後である = false;
}
}
if( this._活性化した直後である )
{
this._活性化した直後である = false;
+ this._描画開始チップ番号 = 0; // -1 → 0; 演奏開始。
#region " 演奏開始時刻を初期化し、動画とBGMの再生を開始する。"
//----------------
Log.Info( $"演奏開始の先頭からの時間: {演奏開始位置の先頭からの時間sec} sec" );
// その他のオプション。
- this._Autoチップのドラム音を再生する = msg.ドラムチップのヒット時に発声する;
+ App.システム設定.Autoチップのドラム音を再生する = msg.ドラムチップのヒット時に発声する;
}
// BGMと動画を(必要あれば)再生開始。
#endregion
this._FPS = new FPS();
+
+ // 高頻度進行タスクを生成、実行開始。
+ Task.Factory.StartNew( this._高頻度進行処理タスクエントリ );
}
//----------------
#endregion
}
//----------------
#endregion
+ this._FPS.VPSをカウントする();
this._ステージ台.描画する( dr, 0f, 0f );
this._レーンフレーム.進行描画する( dr );
- this._ヒット判定バー.描画する( dr, 597f, 869f );
+ this._ヒット判定バー.描画する( dr, 597f, ヒット判定バーの中央Y座標dpx - 43f );
this._ドラムセット.進行描画する( dr );
break;
{
// todo: this._コンボ.COMBO値 = 0;
- this._描画開始チップ番号 = -1; // 演奏停止
+ this._描画開始チップ番号 = -1; // 演奏停止
this.BGMを停止する();
this._背景動画開始済み = false;
}
}
- private bool _活性化した直後である;
+ private const float ヒット判定バーの中央Y座標dpx = 869f + 43f;
private double _演奏開始時刻sec = 0.0;
- private bool _Autoチップのドラム音を再生する = true;
+ private double _現在進行描画中の譜面スクロール速度の倍率 = 1.0;
/// <summary>
/// 演奏スコア.チップリスト[] のうち、描画を始めるチップのインデックス番号。
/// </summary>
/// <remarks>
/// 演奏開始直後は 0 で始まり、対象番号のチップが描画範囲を流れ去るたびに +1 される。
+ /// このメンバの更新は、高頻度進行タスクではなく、進行描画メソッドで行う。(低精度で構わないので)
/// </remarks>
private int _描画開始チップ番号 = -1;
private bool _BGM再生開始済み = false;
- private FPS _FPS = null;
-
private 画像 _ステージ台 = null;
private レーンフレーム _レーンフレーム = null;
private 画像 _ヒット判定バー = null;
+ private 画像 _チップ画像 = null;
+
+ private 矩形リスト _チップ画像の矩形リスト = null;
+
+ private コンソールフォント _コンソールフォント = null;
+
+ private ドラムサウンド _ドラムサウンド = null;
+
+ private bool _活性化した直後である;
+
+ private FPS _FPS = null;
+
+ /// <summary>
+ /// OFF:タスク未生成、ON:タスク稼働中、無効:タスク終了済み
+ /// </summary>
+ private TriStateEvent _高頻度進行タスクへのイベント = null;
+
private readonly object _スレッド間同期 = new object();
}
}
}
+
+
+ // 高頻度進行処理関連。
+
+ /// <summary>
+ /// 音声の再生と演奏用入力を行う。
+ /// </summary>
+ /// <remarks>
+ /// 描画処理とは独立したタスクを使い、より高頻度にループさせる。
+ /// </remarks>
+ private void _高頻度進行処理タスクエントリ()
+ {
+ Log.Info( "高頻度進行処理タスクを開始します。" );
+
+ this._高頻度進行タスクへのイベント.現在の状態 = TriStateEvent.状態種別.ON;
+
+ while( this._高頻度進行タスクへのイベント.現在の状態 == TriStateEvent.状態種別.ON )
+ {
+ lock( this._スレッド間同期 )
+ {
+ // 進行。
+
+ this._FPS.FPSをカウントしプロパティを更新する();
+
+ #region " 自動ヒット処理。"
+ //----------------
+ this._描画範囲のチップに処理を適用する( ( chip, ヒット判定バーとの時間sec, ヒット判定バーとの距離dpx ) => {
+
+ var オプション設定 = App.ユーザ管理.選択されているユーザ.オプション設定;
+ var 対応表 = オプション設定.ドラムとチップと入力の対応表.対応表[ chip.チップ種別 ];
+ var AutoPlay = オプション設定.AutoPlay[ 対応表.AutoPlay種別 ];
+
+ bool チップはヒット済みである = chip.ヒット済みである;
+ bool チップはMISSエリアに達している = ( ヒット判定バーとの時間sec > オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] );
+ bool チップはヒット判定バーを通過した = ( 0 <= ヒット判定バーとの距離dpx );
+
+ if( チップはヒット済みである )
+ {
+ // 何もしない。
+ return;
+ }
+
+ if( チップはMISSエリアに達している )
+ {
+ // MISS判定。
+ if( AutoPlay && 対応表.AutoPlayON.MISS判定 )
+ {
+ this._チップのヒット処理を行う( chip, ヒットランク種別.MISS, 対応表.AutoPlayON.自動ヒット時処理 );
+ return;
+ }
+ else if( !AutoPlay && 対応表.AutoPlayOFF.MISS判定 )
+ {
+ this._チップのヒット処理を行う( chip, ヒットランク種別.MISS, 対応表.AutoPlayOFF.自動ヒット時処理 );
+ return;
+ }
+ else
+ {
+ // 通過。
+ }
+ }
+
+ if( チップはヒット判定バーを通過した )
+ {
+ // 自動ヒット判定。
+ if( AutoPlay && 対応表.AutoPlayON.自動ヒット )
+ {
+ this._チップのヒット処理を行う( chip, ヒットランク種別.AUTO, 対応表.AutoPlayON.自動ヒット時処理 );
+ return;
+ }
+ else if( !AutoPlay && 対応表.AutoPlayOFF.自動ヒット )
+ {
+ this._チップのヒット処理を行う( chip, ヒットランク種別.AUTO, 対応表.AutoPlayOFF.自動ヒット時処理 );
+ return;
+ }
+ else
+ {
+ // 通過。
+ }
+ }
+
+ } );
+ //----------------
+ #endregion
+
+ // 入力。
+
+ App.入力管理.すべての入力デバイスをポーリングする( 入力履歴を記録する: false );
+
+ if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Escape ) )
+ {
+ #region " ESC → ステージキャンセル "
+ //----------------
+ if( App.ビュアーモードではない )
+ {
+ this.BGMを停止する();
+ this.現在のフェーズ = フェーズ.キャンセル;
+ break; // このタスクを終了。
+ }
+ else
+ {
+ // ビュアーモード時のキャンセルは無効。
+ }
+ //----------------
+ #endregion
+ }
+ if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Up ) )
+ {
+ #region " 上 → 譜面スクロールを加速 "
+ //----------------
+ const double 最大倍率 = 8.0;
+ App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 =
+ Math.Min( App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 + 0.5, 最大倍率 );
+ //----------------
+ #endregion
+ }
+ if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Down ) )
+ {
+ #region " 下 → 譜面スクロールを減速 "
+ //----------------
+ const double 最小倍率 = 0.5;
+ App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 =
+ Math.Max( App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 - 0.5, 最小倍率 );
+ //----------------
+ #endregion
+ }
+
+ #region " ユーザヒット処理。"
+ //----------------
+ foreach( var 入力 in App.入力管理.ポーリング結果 )
+ {
+ if( 入力.InputEvent.離された )
+ continue; // 押下イベントじゃないなら無視。
+
+ // todo: チップに対応するパッドのアニメを開始。
+ }
+
+ var 処理済み入力 = new List<ドラム入力イベント>(); // ヒット処理が終わった入力は、二重処理しないよう、この中に追加しておく。
+
+ this._描画範囲のチップに処理を適用する( ( chip, ヒット判定バーとの時間sec, ヒット判定バーとの距離dpx ) => {
+
+ var オプション設定 = App.ユーザ管理.選択されているユーザ.オプション設定;
+ var 対応表 = オプション設定.ドラムとチップと入力の対応表.対応表[ chip.チップ種別 ];
+ var AutoPlay = オプション設定.AutoPlay[ 対応表.AutoPlay種別 ];
+ var ヒット判定バーとの時間の絶対値sec = Math.Abs( ヒット判定バーとの時間sec );
+
+ bool チップはヒット済みである = chip.ヒット済みである;
+ bool チップはMISSエリアに達している = ( ヒット判定バーとの時間sec > オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] );
+ bool チップはヒット可能エリアにある = ( ヒット判定バーとの時間sec >= -( オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] ) && !チップはMISSエリアに達している );
+
+ if( AutoPlay || チップはヒット済みである || !( 対応表.AutoPlayOFF.ユーザヒット ) || !( チップはヒット可能エリアにある ) )
+ return;
+
+ // チップにヒットする入力を探す。
+
+ // todo: シンバルフリーを実装する。
+ var ヒット入力 = App.入力管理.ポーリング結果.FirstOrDefault( ( 入力 ) =>
+ ( 入力.InputEvent.押された ) &&
+ ( 対応表.ドラム入力種別 == 入力.Type ) &&
+ !( 処理済み入力.Contains( 入力 ) )
+ );
+
+ if( null == ヒット入力 )
+ {
+ return;
+ }
+ else
+ {
+ 処理済み入力.Add( ヒット入力 );
+ }
+
+ // ヒットランクを判定する。
+
+ var ヒットランク = ヒットランク種別.POOR;
+
+ if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.PERFECT ] )
+ {
+ ヒットランク = ヒットランク種別.PERFECT;
+ }
+ else if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.GREAT ] )
+ {
+ ヒットランク = ヒットランク種別.GREAT;
+ }
+ else if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.GOOD ] )
+ {
+ ヒットランク = ヒットランク種別.GOOD;
+ }
+
+ // ヒット処理。
+
+ this._チップのヒット処理を行う( chip, ヒットランク, 対応表.AutoPlayOFF.ユーザヒット時処理 );
+
+ } );
+ //----------------
+ #endregion
+ }
+
+ // ウェイト
+ Thread.Sleep( 1 );
+ }
+
+ this._高頻度進行タスクへのイベント.現在の状態 = TriStateEvent.状態種別.無効;
+
+ Log.Info( "高頻度進行処理タスクを終了しました。" );
+ }
+
+ /// <param name="適用する処理">引数は、順に、対象のチップ、ヒット判定バーとの時間sec、ヒット判定バーとの距離dpx</param>
+ /// <remarks>
+ /// 「描画範囲のチップ」とは、<see cref="_描画開始チップ番号"/> から、画面上端をはみ出すまでの間のチップを指す。
+ /// </remarks>
+ private void _描画範囲のチップに処理を適用する( Action<チップ, double, double> 適用する処理 )
+ {
+ lock( this._スレッド間同期 )
+ {
+ var スコア = App.演奏スコア;
+ if( null == スコア )
+ return;
+
+ double リアルタイム演奏時刻sec = this._演奏開始からの経過時間secを返す();
+
+ for( int i = this._描画開始チップ番号; ( 0 <= i ) && ( i < スコア.チップリスト.Count ); i++ )
+ {
+ var チップ = スコア.チップリスト[ i ];
+
+ // ヒット判定バーとチップの間の、時間 と 距離 を算出。→ いずれも、負数ならバー未達、0でバー直上、正数でバー通過。
+ double ヒット判定バーとの時間sec = リアルタイム演奏時刻sec - チップ.描画時刻sec;
+ double ヒット判定バーとの距離dpx = スコア.指定された時間secに対応する符号付きピクセル数を返す( this._現在進行描画中の譜面スクロール速度の倍率, ヒット判定バーとの時間sec );
+
+ // 終了判定。
+ bool チップは画面上端より上に出ている = ( (ヒット判定バーの中央Y座標dpx + ヒット判定バーとの距離dpx ) < -40.0 ); // -40dpx はチップが隠れるであろう適当なマージン。
+ if( チップは画面上端より上に出ている )
+ break;
+
+ // 処理実行。
+ 適用する処理( チップ, ヒット判定バーとの時間sec, ヒット判定バーとの距離dpx );
+ }
+ }
+ }
+
+ private void _チップのヒット処理を行う( チップ chip, ヒットランク種別 ヒットランク, ドラムとチップと入力の対応表.Column.Columnヒット処理 ヒット処理表 )
+ {
+ lock( this._スレッド間同期 )
+ {
+ chip.ヒット済みである = true;
+
+ if( ヒット処理表.再生 )
+ {
+ #region " チップを再生する。"
+ //----------------
+ if( chip.チップ種別 == チップ種別.背景動画 )
+ {
+ // 背景動画の再生を開始する。
+ this._背景動画?.再生を開始する();
+ this._背景動画開始済み = true;
+
+ // BGMの再生を開始する。
+ this._BGM?.Play();
+ this._BGM再生開始済み = true;
+ }
+ else
+ {
+ if( App.システム設定.Autoチップのドラム音を再生する )
+ this._ドラムサウンド.発声する( chip.チップ種別, ( chip.音量 / ( float ) チップ.最大音量 ) );
+ }
+ //----------------
+ #endregion
+ }
+ if( ヒット処理表.判定 )
+ {
+ #region " チップの判定処理を行う。"
+ //----------------
+ if( ヒットランク != ヒットランク種別.MISS )
+ {
+ // todo: this._回転羽.発火する( chip.チップ種別 );
+ // todo: this._コンボ.COMBO値++;
+ // todo: this.ヒットした回数[ hitType ]++;
+ // todo: this._ヒット判定文字列.表示開始( chipType, hitType );
+ }
+ else
+ {
+ // todo: this._コンボ.COMBO値 = 0;
+ // todo: this._ヒット判定文字列.表示開始( chipType, ヒットランク種別.MISS );
+ }
+ //----------------
+ #endregion
+ }
+ if( ヒット処理表.非表示 )
+ {
+ #region " チップを非表示にする。"
+ //----------------
+ if( ヒットランク != ヒットランク種別.MISS )
+ {
+ chip.可視 = false; // PERFECT~POOR チップは非表示。
+ }
+ else
+ {
+ if( chip.可視 )
+ chip.可視 = false; // MISSチップは最後まで表示し続ける。
+ }
+ //----------------
+ #endregion
+ }
+ }
+ }
}
}
/// SSTユーザごとに設定。
/// </summary>
[DataContract( Name = "Options", Namespace = "" )]
+ [KnownType( typeof( 表示レーンの左右 ) )]
+ [KnownType( typeof( ヒットランク種別 ) )]
+ [KnownType( typeof( AutoPlay種別 ) )]
+ [KnownType( typeof( 動画表示パターン種別 ) )]
class オプション設定 : IExtensibleDataObject
{
/// <summary>
/// <summary>
/// オプション設定を保存する XML ファイルのパス。
/// </summary>
- private static readonly string _OptionXmlファイル名 = @"Options.xml"; // パスなし
+ private static readonly string _OptionXmlファイル名 = @"Options.xml"; // パスなし
- private IReadOnlyDictionary<ヒットランク種別, double> _最大ヒット距離secの既定値 = new Dictionary<ヒットランク種別, double>() {
- { ヒットランク種別.PERFECT, 0.034 },
- { ヒットランク種別.GREAT, 0.067 },
- { ヒットランク種別.GOOD, 0.084 },
- { ヒットランク種別.POOR, 0.117 },
- { ヒットランク種別.MISS, double.NaN }, // 使わない
- { ヒットランク種別.AUTO, double.NaN }, // 使わない
- };
+ private Dictionary<ヒットランク種別, double> _最大ヒット距離secの既定値 = null;
/// <summary>
/// コンストラクタ時、または逆シリアル化時のメンバの既定値を設定する。
this.ドラムとチップと入力の対応表 = new ドラムとチップと入力の対応表( this.表示レーンの左右 );
+ // ※メンバ初期化子で設定してはならない。(OnDeserializing 時にはコンストラクタが呼び出されない。)
+ this._最大ヒット距離secの既定値 = new Dictionary<ヒットランク種別, double>() {
+ { ヒットランク種別.PERFECT, 0.034 },
+ { ヒットランク種別.GREAT, 0.067 },
+ { ヒットランク種別.GOOD, 0.084 },
+ { ヒットランク種別.POOR, 0.117 },
+ { ヒットランク種別.MISS, double.NaN }, // 使わない
+ { ヒットランク種別.AUTO, double.NaN }, // 使わない
+ };
+
this.最大ヒット距離sec = new Dictionary<ヒットランク種別, double>();
foreach( ヒットランク種別 hr in Enum.GetValues( typeof( ヒットランク種別 ) ) )
this.最大ヒット距離sec[ hr ] = this._最大ヒット距離secの既定値[ hr ];