diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GameInputBodyMotionController.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GameInputBodyMotionController.cs index 2ba8c420f..af7db90e3 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GameInputBodyMotionController.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GameInputBodyMotionController.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Threading; using Baku.VMagicMirror.GameInput; +using Cysharp.Threading.Tasks; using UniRx; using UnityEngine; using Zenject; @@ -21,6 +24,12 @@ public class GameInputBodyMotionController : PresenterBase, ITickable //スティックを上下に倒したとき顔が上下に向く量(deg) private const float HeadPitchMaxDeg = 25f; + //Hipsの並進が急だと違和感が出るので、ボーン回転だけシャープに補間させる + private const float CustomMotionFadeInDuration = 0.05f; + private const float CustomMotionFadeOutDuration = 0.25f; + private const float CustomMotionHipFadeInDuration = 0.25f; + private const float CustomMotionHipFadeOutDuration = 0.25f; + private static readonly int Active = Animator.StringToHash("Active"); private static readonly int Jump = Animator.StringToHash("Jump"); private static readonly int Punch = Animator.StringToHash("Punch"); @@ -36,7 +45,14 @@ public class GameInputBodyMotionController : PresenterBase, ITickable private readonly BodyMotionModeController _bodyMotionModeController; private readonly GameInputBodyRootOrientationController _rootOrientationController; private readonly GameInputSourceSet _sourceSet; + private readonly VrmaRepository _vrmaRepository; + private readonly VrmaMotionSetter _vrmaMotionSetter; + private readonly VrmaMotionSetterLocker _vrmaMotionSetterLocker = new(); + private readonly CancellationTokenSource _cts = new(); + private readonly HashSet _customMotionActionKeys = new(); + private bool _customMotionActionKeysInitialized; + private bool _hasModel; private Animator _animator; private int _baseLayerIndex; @@ -45,6 +61,7 @@ public class GameInputBodyMotionController : PresenterBase, ITickable private GameInputLocomotionStyle _locomotionStyle = GameInputLocomotionStyle.FirstPerson; private bool _alwaysRun = true; private bool _bodyMotionActive; + private bool _customMotionRunning; private Vector2 _rawMoveInput; private Vector2 _rawLookAroundInput; @@ -73,6 +90,8 @@ public GameInputBodyMotionController( IVRMLoadable vrmLoadable, IMessageReceiver receiver, BodyMotionModeController bodyMotionModeController, + VrmaRepository vrmaRepository, + VrmaMotionSetter vrmaMotionSetter, GameInputBodyRootOrientationController rootOrientationController, GameInputSourceSet sourceSet ) @@ -80,6 +99,8 @@ GameInputSourceSet sourceSet _vrmLoadable = vrmLoadable; _receiver = receiver; _bodyMotionModeController = bodyMotionModeController; + _vrmaRepository = vrmaRepository; + _vrmaMotionSetter = vrmaMotionSetter; _rootOrientationController = rootOrientationController; _sourceSet = sourceSet; } @@ -112,6 +133,11 @@ public override void Initialize() .Subscribe(_ => TryAct(Punch)) .AddTo(this); + Observable.Merge(_sourceSet.Sources.Select(s => s.CustomMotion)) + .ThrottleFirst(TimeSpan.FromSeconds(0.2f)) + .Subscribe(TryRunCustomMotion) + .AddTo(this); + //NOTE: 2デバイスから同時に来るのは許容したうえで、 //ゲームパッド側はスティックが0付近のとき何も飛んでこない、というのを期待してる Observable.Merge( @@ -145,6 +171,13 @@ public override void Initialize() .AddTo(this); } + public override void Dispose() + { + base.Dispose(); + _cts.Cancel(); + _cts.Dispose(); + } + private void OnVrmLoaded(VrmLoadedInfo obj) { _animator = obj.animator; @@ -176,13 +209,109 @@ private void TryAct(int triggerHash) var stateHash = _animator.GetCurrentAnimatorStateInfo(_baseLayerIndex).shortNameHash; - //要するにアクション中は無視する…というガード - if (stateHash == Walk || stateHash == Run || stateHash == Crouch) + //アクション中は無視する + if ((stateHash == Walk || stateHash == Run || stateHash == Crouch) && + !_customMotionRunning + ) { _animator.SetTrigger(triggerHash); } } + private void TryRunCustomMotion(string actionKey) + { + if (!_hasModel || !_bodyMotionActive || !IsAvailableCustomMotionKey(actionKey)) + { + return; + } + + var item = _vrmaRepository + .GetAvailableFileItems() + .First(i => i.FileName == actionKey); + + var stateHash = _animator.GetCurrentAnimatorStateInfo(_baseLayerIndex).shortNameHash; + + //要するにアクション中は無視する…というガード + //CustomMotion中のCustomMotion呼び出しも許可しない + if (!(stateHash == Walk || stateHash == Run || stateHash == Crouch) || + _customMotionRunning) + { + return; + } + + if (!_vrmaMotionSetter.TryLock(_vrmaMotionSetterLocker)) + { + return; + } + + _customMotionRunning = true; + RunCustomMotionAsync(item, _cts.Token).Forget(); + } + + private async UniTaskVoid RunCustomMotionAsync(VrmaFileItem item, CancellationToken ct) + { + _vrmaMotionSetter.FixHipLocalPosition = false; + + //やること: 適用率を0 > 1 > 0に遷移させつつ適用していく + //prevのアニメーションを適用するかどうかは動的にチェックして決める + _vrmaRepository.Run(item, false); + var anim = _vrmaRepository.PeekInstance; + var animDuration = _vrmaRepository.PeekInstance.Duration; + var count = 0f; + while (count < animDuration) + { + var rate = 1f; + var hipRate = 1f; + + if (count > animDuration - CustomMotionHipFadeOutDuration) + { + //終了間際, 1->0に下がっていく + _vrmaRepository.StopPrevAnimation(); + hipRate = Mathf.Clamp01((animDuration - count) / CustomMotionHipFadeOutDuration); + } + else if (count < CustomMotionHipFadeInDuration) + { + //0 -> 1, 始まってすぐ + hipRate = Mathf.Clamp01(count / CustomMotionHipFadeInDuration); + } + else + { + // 中間部分。このタイミングで補間が要らなくなるので明示的に宣言しておく + _vrmaRepository.StopPrevAnimation(); + } + + if (count > animDuration - CustomMotionFadeOutDuration) + { + // 終了間近 + rate = Mathf.Clamp01((animDuration - count) / CustomMotionFadeOutDuration); + } + else if (count < CustomMotionFadeInDuration) + { + // 始まってすぐ + rate = Mathf.Clamp01(count / CustomMotionFadeInDuration); + } + + //NOTE: rate == 1とかrate == 0のケースの最適化はmotionSetterにケアさせる + if (_vrmaRepository.PrevInstance is { IsPlaying: true } playingPrev) + { + //VRMAどうしの補間中にしか通らないパスで、通るのは珍しい + _vrmaMotionSetter.Set(playingPrev, anim, rate, hipRate); + } + else + { + _vrmaMotionSetter.Set(anim, rate, hipRate); + } + + //NOTE: LateTick相当くらいのタイミングを狙っていることに注意 + await UniTask.NextFrame(ct); + count += Time.deltaTime; + } + + _vrmaRepository.StopCurrentAnimation(); + _vrmaMotionSetter.ReleaseLock(); + _customMotionRunning = false; + } + private void SetActiveness(bool active) { if (active == _bodyMotionActive) @@ -221,6 +350,16 @@ private void SetLocomotionStyle(int rawStyleValue) _moveInputDampSpeed = Vector2.zero; } + + private bool IsAvailableCustomMotionKey(string actionKey) + { + if (!_customMotionActionKeysInitialized) + { + _customMotionActionKeys.UnionWith(_vrmaRepository.GetAvailableMotionNames()); + _customMotionActionKeysInitialized = true; + } + return _customMotionActionKeys.Contains(actionKey); + } private void ResetParameters() { diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GameInputEnums.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GameInputEnums.cs index 9057e9979..acd9f917e 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GameInputEnums.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GameInputEnums.cs @@ -18,7 +18,8 @@ public enum GameInputStickAction public enum GameInputButtonAction { - None, + Custom = -1, + None = 0, Jump, Crouch, Run, diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GamepadGameInputKeyAssign.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GamepadGameInputKeyAssign.cs index 5edb56453..af73693a4 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GamepadGameInputKeyAssign.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GamepadGameInputKeyAssign.cs @@ -1,9 +1,18 @@ using System; +using UnityEngine; namespace Baku.VMagicMirror.GameInput { //WPF側の定義と揃えてることに注意(デフォルト値も含めて) + [Serializable] + public class GameInputCustomAction + { + public string CustomKey; + + public static GameInputCustomAction Empty() => new() { CustomKey = "" }; + } + [Serializable] public class GamepadGameInputKeyAssign { @@ -25,6 +34,32 @@ public class GamepadGameInputKeyAssign public GameInputStickAction StickLeft; public GameInputStickAction StickRight; - public static GamepadGameInputKeyAssign LoadDefault() => new GamepadGameInputKeyAssign(); + [SerializeField] private GameInputCustomAction CustomButtonA; + [SerializeField] private GameInputCustomAction CustomButtonB; + [SerializeField] private GameInputCustomAction CustomButtonX; + [SerializeField] private GameInputCustomAction CustomButtonY; + + [SerializeField] private GameInputCustomAction CustomButtonLButton; + [SerializeField] private GameInputCustomAction CustomButtonLTrigger; + [SerializeField] private GameInputCustomAction CustomButtonRButton; + [SerializeField] private GameInputCustomAction CustomButtonRTrigger; + + [SerializeField] private GameInputCustomAction CustomButtonView; + [SerializeField] private GameInputCustomAction CustomButtonMenu; + + public string CustomButtonAKey => CustomButtonA?.CustomKey ?? ""; + public string CustomButtonBKey => CustomButtonB?.CustomKey ?? ""; + public string CustomButtonXKey => CustomButtonX?.CustomKey ?? ""; + public string CustomButtonYKey => CustomButtonY?.CustomKey ?? ""; + + public string CustomButtonLButtonKey => CustomButtonLButton?.CustomKey ?? ""; + public string CustomButtonLTriggerKey => CustomButtonLTrigger?.CustomKey ?? ""; + public string CustomButtonRButtonKey => CustomButtonRButton?.CustomKey ?? ""; + public string CustomButtonRTriggerKey => CustomButtonRTrigger?.CustomKey ?? ""; + + public string CustomButtonViewKey => CustomButtonView?.CustomKey ?? ""; + public string CustomButtonMenuKey => CustomButtonMenu?.CustomKey ?? ""; + + public static GamepadGameInputKeyAssign LoadDefault() => new(); } } diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GamepadGameInputSource.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GamepadGameInputSource.cs index 4a8be6c7b..64ea7b035 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GamepadGameInputSource.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/GamepadGameInputSource.cs @@ -18,6 +18,7 @@ public class GamepadGameInputSource : PresenterBase, IGameInputSource IObservable IGameInputSource.GunFire => _gunFire; IObservable IGameInputSource.Jump => _jump; IObservable IGameInputSource.Punch => _punch; + IObservable IGameInputSource.CustomMotion => _customMotion; #endregion @@ -25,14 +26,15 @@ public class GamepadGameInputSource : PresenterBase, IGameInputSource private readonly IMessageReceiver _receiver; private CompositeDisposable _disposable; - private readonly ReactiveProperty _moveInput = new ReactiveProperty(); - private readonly ReactiveProperty _lookAroundInput = new ReactiveProperty(); - private readonly ReactiveProperty _isCrouching = new ReactiveProperty(); - private readonly ReactiveProperty _isRunning = new ReactiveProperty(); - private readonly ReactiveProperty _gunFire = new ReactiveProperty(); - private readonly Subject _jump = new Subject(); - private readonly Subject _punch = new Subject(); - + private readonly ReactiveProperty _moveInput = new(); + private readonly ReactiveProperty _lookAroundInput = new(); + private readonly ReactiveProperty _isCrouching = new(); + private readonly ReactiveProperty _isRunning = new(); + private readonly ReactiveProperty _gunFire = new(); + private readonly Subject _jump = new(); + private readonly Subject _punch = new(); + private readonly Subject _customMotion = new(); + private bool _isActive; private GamepadGameInputKeyAssign _keyAssign = GamepadGameInputKeyAssign.LoadDefault(); @@ -172,6 +174,13 @@ private void OnButtonUpDown(GamepadKeyData data) case GameInputButtonAction.Jump: _jump.OnNext(Unit.Default); break; + case GameInputButtonAction.Custom: + var key = GetButtonActionCustomKey(data.Key); + if (!string.IsNullOrEmpty(key)) + { + _customMotion.OnNext(key); + } + break; } } @@ -228,6 +237,24 @@ private GameInputButtonAction GetButtonAction(GamepadKey key) }; } + private string GetButtonActionCustomKey(GamepadKey key) + { + return key switch + { + GamepadKey.A => _keyAssign.CustomButtonAKey, + GamepadKey.B => _keyAssign.CustomButtonBKey, + GamepadKey.X => _keyAssign.CustomButtonXKey, + GamepadKey.Y => _keyAssign.CustomButtonYKey, + GamepadKey.LShoulder => _keyAssign.CustomButtonLButtonKey, + GamepadKey.LTrigger => _keyAssign.CustomButtonLTriggerKey, + GamepadKey.RShoulder => _keyAssign.CustomButtonRButtonKey, + GamepadKey.RTrigger=> _keyAssign.CustomButtonRTriggerKey, + GamepadKey.Select => _keyAssign.CustomButtonViewKey, + GamepadKey.Start => _keyAssign.CustomButtonMenuKey, + _ => "", + }; + } + public override void Dispose() { base.Dispose(); diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/IGameInputSource.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/IGameInputSource.cs index d9580172e..f22464e1b 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/IGameInputSource.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/IGameInputSource.cs @@ -28,5 +28,8 @@ public interface IGameInputSource IObservable Jump { get; } IObservable Punch { get; } + + /// .vrma のカスタムモーションはココからkeyを指定して発火 + IObservable CustomMotion { get; } } } diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/KeyboardGameInputKeyAssign.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/KeyboardGameInputKeyAssign.cs index cbeebb1c3..be6611dd8 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/KeyboardGameInputKeyAssign.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/KeyboardGameInputKeyAssign.cs @@ -3,6 +3,15 @@ namespace Baku.VMagicMirror.GameInput { + [Serializable] + public class KeyboardGameInputCustomAction + { + public GameInputCustomAction CustomAction; + public string KeyCode; + + public string CustomActionKey => CustomAction?.CustomKey ?? ""; + } + [Serializable] public class KeyboardGameInputKeyAssign { @@ -26,7 +35,7 @@ public class KeyboardGameInputKeyAssign public string TriggerKeyCode = ""; public string PunchKeyCode = ""; - public static KeyboardGameInputKeyAssign LoadDefault() => new KeyboardGameInputKeyAssign(); + public KeyboardGameInputCustomAction[] CustomActions; public void OverwriteKeyCodeIntToKeyName() { @@ -35,8 +44,18 @@ public void OverwriteKeyCodeIntToKeyName() CrouchKeyCode = ParseIntToKeyName(CrouchKeyCode); TriggerKeyCode = ParseIntToKeyName(TriggerKeyCode); PunchKeyCode = ParseIntToKeyName(PunchKeyCode); + + foreach (var ca in CustomActions) + { + ca.KeyCode = ParseIntToKeyName(ca.KeyCode); + } } + public static KeyboardGameInputKeyAssign LoadDefault() => new() + { + CustomActions = Array.Empty(), + }; + private static string ParseIntToKeyName(string key) => string.IsNullOrEmpty(key) ? "" : int.TryParse(key, out var value) ? ((Keys)value).ToString() : diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/KeyboardGameInputSource.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/KeyboardGameInputSource.cs index 8a14beab3..e80165034 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/KeyboardGameInputSource.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/Motion/GamePadBasedBody/KeyboardGameInputSource.cs @@ -22,17 +22,19 @@ public class KeyboardGameInputSource : PresenterBase, ITickable, IGameInputSourc IObservable IGameInputSource.GunFire => _gunFire; IObservable IGameInputSource.Jump => _jump; IObservable IGameInputSource.Punch => _punch; + IObservable IGameInputSource.CustomMotion => _customMotion; #endregion private bool _isActive; - private readonly ReactiveProperty _moveInput = new ReactiveProperty(); - private readonly ReactiveProperty _lookAroundInput = new ReactiveProperty(); - private readonly ReactiveProperty _isCrouching = new ReactiveProperty(); - private readonly ReactiveProperty _isRunning = new ReactiveProperty(); - private readonly ReactiveProperty _gunFire = new ReactiveProperty(); - private readonly Subject _jump = new Subject(); - private readonly Subject _punch = new Subject(); + private readonly ReactiveProperty _moveInput = new(); + private readonly ReactiveProperty _lookAroundInput = new(); + private readonly ReactiveProperty _isCrouching = new(); + private readonly ReactiveProperty _isRunning = new(); + private readonly ReactiveProperty _gunFire = new(); + private readonly Subject _jump = new(); + private readonly Subject _punch = new(); + private readonly Subject _customMotion = new(); private readonly IKeyMouseEventSource _keySource; private readonly IMessageReceiver _receiver; @@ -259,6 +261,17 @@ private void OnKeyDown(string key) _punch.OnNext(Unit.Default); } + foreach (var custom in _keyAssign.CustomActions) + { + if (custom.KeyCode == key) + { + var actionKey = custom.CustomActionKey; + if (!string.IsNullOrEmpty(actionKey)) + { + _customMotion.OnNext(actionKey); + } + } + } if (_useWasdMove) { diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/V2/WordToMotionPresenter.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/V2/WordToMotionPresenter.cs index 569046d36..6be7e0a38 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/V2/WordToMotionPresenter.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/V2/WordToMotionPresenter.cs @@ -68,19 +68,10 @@ public override void Initialize() // VmmCommands.RequestCustomMotionDoctor, // _ => { } // ); - + _receiver.AssignQueryHandler( VmmQueries.GetAvailableCustomMotionClipNames, - q => - { - //カスタムモーションと呼ばれるものがvmm_motionとvrmaの2種類ある - q.Result = string.Join("\t", - _customMotionRepository - .LoadAvailableCustomMotionNames() - .Concat(_vrmaRepository.GetAvailableMotionNames()) - ); - Debug.Log("Get Available CustomMotion Clip Names, result = " + q.Result); - }); + SetAvailableMotionClipNames); //リクエストは等価に扱う && 頻度は制御する、という話 _sources @@ -115,7 +106,7 @@ public override void Initialize() _vrmaRepository.Initialize(); } - + private void SetWordToMotionInputType(int deviceType) { var type = (SourceType) deviceType; @@ -160,5 +151,24 @@ private void ReceiveWordToMotionPreviewInfo(string json) LogOutput.Instance.Write(ex); } } + + private void SetAvailableMotionClipNames(ReceivedQuery q) + { + //カスタムモーションと呼ばれるものがvmm_motionとvrmaの2種類あるが、引数によってはvrmaのみを返却する + var vrmaOnly = q.ToBoolean(); + if (vrmaOnly) + { + q.Result = string.Join("\t", _vrmaRepository.GetAvailableMotionNames()); + } + else + { + q.Result = string.Join("\t", + _customMotionRepository + .LoadAvailableCustomMotionNames() + .Concat(_vrmaRepository.GetAvailableMotionNames()) + ); + } + Debug.Log("Get Available CustomMotion Clip Names, result = " + q.Result); + } } } diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/VrmaMotionPlayer.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/VrmaMotionPlayer.cs index 67b184a6b..c91fb2fdd 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/VrmaMotionPlayer.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/VrmaMotionPlayer.cs @@ -24,6 +24,7 @@ public VrmaMotionPlayer( private readonly IVRMLoadable _vrmLoadable; private readonly VrmaRepository _repository; private readonly VrmaMotionSetter _motionSetter; + private readonly VrmaMotionSetterLocker _motionSetterLocker = new(); private bool _hasModel; private bool _playing; @@ -80,6 +81,13 @@ void IWordToMotionPlayer.Play(MotionRequest request, out float duration) return; } + //ゲーム入力のほうでVRMAが再生中だと反応できないようにしておく + //※Word to Motionを常時優先する説もある (普段は何でもかんでもWtMが最優先ではあるので) + if (!_motionSetter.TryLock(_motionSetterLocker)) + { + return; + } + //NOTE: I/Fの戻り値としてはIK FadeInするの時間を引いて答えておく duration -= FadeDuration; @@ -89,7 +97,8 @@ void IWordToMotionPlayer.Play(MotionRequest request, out float duration) RunAnimationAsync(targetItem, _cts.Token).Forget(); } - //TODO: Game InputでもVRMAを使う場合、この方法で止めるのはNG + //NOTE: ゲーム入力とWtMで同じモーションを使うとちょっと変になる可能性がある。 + //いったんマイナーケースだと思って許容している public void Stop() { if (!_hasModel) @@ -180,6 +189,8 @@ private void OnModelDisposed() private async UniTaskVoid RunAnimationAsync(VrmaFileItem item, CancellationToken cancellationToken) { + _motionSetter.FixHipLocalPosition = true; + //やること: 適用率を0 > 1 > 0に遷移させつつ適用していく //prevのアニメーションを適用するかどうかは動的にチェックして決める _repository.Run(item, false); @@ -245,6 +256,7 @@ private async UniTaskVoid StopAsync(CancellationToken cancellationToken) finally { _stopRunning = false; + _motionSetter.ReleaseLock(); } } @@ -262,6 +274,7 @@ private void StopImmediate() _repository.StopCurrentAnimation(); _playing = false; _playingPreview = false; + _motionSetter.ReleaseLock(); } //NOTE: モーション名として拡張子付きのファイル名が使われている事を期待している diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/VrmaMotionSetter.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/VrmaMotionSetter.cs index b04173efe..28c2095e1 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/VrmaMotionSetter.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/AvatarControl/WordToMotion/VrmaMotionSetter.cs @@ -5,10 +5,13 @@ namespace Baku.VMagicMirror { + public sealed class VrmaMotionSetterLocker + { + } + public class VrmaMotionSetter : PresenterBase { private const int BoneMax = (int)HumanBodyBones.LastBone; - private const int ResetUpdateFrameCount = 2; struct LateTickContent { @@ -17,6 +20,8 @@ struct LateTickContent public VrmaInstance Prev { get; set; } public VrmaInstance Current { get; set; } public float Rate { get; set; } + //NOTE: Hipだけゆっくり補間しても良い + public float HipRate { get; set; } } private bool _hasModel; @@ -26,7 +31,9 @@ struct LateTickContent private readonly Dictionary _fromCache = new(); //実行順序の関係で、LateTickの処理内容だけ特定した値をキャッシュして用いる private LateTickContent _content; - + + private bool _isLocked = false; + private readonly IVRMLoadable _vrmLoadable; private readonly LateUpdateSourceAfterFinalIK _lateUpdateSource; @@ -55,33 +62,46 @@ private void ApplyUpdate() } _content.HasUpdate = false; - var hipPos = GetHipLocalPosition(); + var hipPos = GetHipFixedLocalPosition(); - if (_content.UsePrevValue && _content.Rate <= 0f) + if (_content.UsePrevValue && _content.Rate <= 0f && _content.Rate <= 0f) { ApplyRawVrma(_content.Prev); - _hips.localPosition = hipPos; + ApplyHipFixedLocalPosition(hipPos); return; } - if (_content.Rate >= 1f) + if (_content.Rate >= 1f && _content.HipRate >= 1f) { ApplyRawVrma(_content.Current); - _hips.localPosition = hipPos; + ApplyHipFixedLocalPosition(hipPos); return; } - //beforeの姿勢をキャッシュする / VRMAどうしを補間する場合は + //beforeの姿勢をキャッシュする / VRMAどうしを補間する場合だけの話 if (_content.UsePrevValue) { ApplyRawVrma(_content.Prev); } CacheRotations(); + hipPos = _hips.localPosition; - //afterの姿勢を適用してからblend + hipsが動かないように元の位置に戻す ApplyRawVrma(_content.Current); - SetBlendedRotations(_content.Rate); - _hips.localPosition = hipPos; + //afterの姿勢を適用してからblend + //このとき「HipRate < 1 だが Rate == 1」というケースでRotationの書き込みをサボると効率がよい + if (_content.Rate < 1f) + { + SetBlendedRotations(_content.Rate); + } + + if (FixHipLocalPosition) + { + ApplyHipFixedLocalPosition(hipPos); + } + else + { + _hips.localPosition = Vector3.Lerp(hipPos, _hips.localPosition, _content.HipRate); + } } private void OnModelLoaded(VrmLoadedInfo info) @@ -115,12 +135,42 @@ private void OnModelUnloaded() _runtime = null; } + public VrmaMotionSetterLocker Locker { get; private set; } + + public bool FixHipLocalPosition { get; set; } + + /// + /// MotionSetterを使うときに呼び出すことで、他クラスから処理を呼ばないようにLockerを設定する + /// + /// + public bool TryLock(VrmaMotionSetterLocker locker) + { + //NOTE: Lock済みのを再Lockするのは成功扱いにすることに注意 + if (Locker != null && Locker != locker) + { + return false; + } + + Locker = locker; + return true; + } + + /// + /// を呼んだクラスがこれを呼ぶことで、クラスを専有している状態を解除する + /// + public void ReleaseLock() + { + Locker = null; + _isLocked = false; + } + /// /// 現在の姿勢に対し、VRMAのモーションを指定した適用率で適用する /// /// /// - public void Set(VrmaInstance anim, float rate) + /// + public void Set(VrmaInstance anim, float rate, float hipRate = -1f) { if (!_hasModel) { @@ -131,7 +181,9 @@ public void Set(VrmaInstance anim, float rate) _content.UsePrevValue = false; _content.Prev = null; _content.Current = anim; - _content.Rate = rate; + var smoothRate = Mathf.SmoothStep(0, 1, rate); + _content.Rate = smoothRate; + _content.HipRate = hipRate >= 0f ? Mathf.SmoothStep(0, 1, hipRate) : smoothRate; } /// @@ -140,7 +192,8 @@ public void Set(VrmaInstance anim, float rate) /// /// /// - public void Set(VrmaInstance prev, VrmaInstance anim, float rate) + /// + public void Set(VrmaInstance prev, VrmaInstance anim, float rate, float hipRate = -1f) { if (!_hasModel) { @@ -151,7 +204,9 @@ public void Set(VrmaInstance prev, VrmaInstance anim, float rate) _content.UsePrevValue = rate < 1f; _content.Prev = prev; _content.Current = anim; - _content.Rate = rate; + var smoothRate = Mathf.SmoothStep(0, 1, rate); + _content.Rate = smoothRate; + _content.HipRate = hipRate >= 0f ? Mathf.SmoothStep(0, 1, hipRate) : smoothRate; } private void ApplyRawVrma(VrmaInstance instance) @@ -161,8 +216,21 @@ private void ApplyRawVrma(VrmaInstance instance) ); } - private Vector3 GetHipLocalPosition() => _bones[HumanBodyBones.Hips].localPosition; + private Vector3 GetHipFixedLocalPosition() => _hips.localPosition; + /// + /// あらかじめGetHipFixedLocalPosition()で取得しておいた値を指定して呼び出す。 + /// Hipsの位置を固定するモードの場合だけ、実際に位置の固定処理として動作する + /// + /// + private void ApplyHipFixedLocalPosition(Vector3 pos) + { + if (FixHipLocalPosition) + { + _hips.localPosition = pos; + } + } + private void CacheRotations() { foreach (var pair in _bones) @@ -171,7 +239,7 @@ private void CacheRotations() } } - //NOTE: 引数は0-1の範囲が前提 + //NOTE: 引数は0-1の範囲を想定しており、0や1ピッタリでは呼ばずに済む方が理想的 // - 0.0: fromCacheの値を使う // - 1.0: 現在の値が優先 private void SetBlendedRotations(float rate) diff --git a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/Interprocess/Model/VmmQueries.cs b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/Interprocess/Model/VmmQueries.cs index 8866c6ff2..6c590c552 100644 --- a/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/Interprocess/Model/VmmQueries.cs +++ b/VMagicMirror/Assets/Baku/VMagicMirror/Scripts/Interprocess/Model/VmmQueries.cs @@ -16,7 +16,7 @@ public static class VmmQueries public const string GetQualitySettingsInfo = nameof(GetQualitySettingsInfo); public const string ApplyDefaultImageQuality = nameof(ApplyDefaultImageQuality); - // Word to Motion + // Word to Motion / GameInput public const string GetAvailableCustomMotionClipNames = nameof(GetAvailableCustomMotionClipNames); } } diff --git a/WPF/VMagicMirrorConfig/Depdency/ModelInstaller.cs b/WPF/VMagicMirrorConfig/Depdency/ModelInstaller.cs index ec1f8855d..e034c4b8f 100644 --- a/WPF/VMagicMirrorConfig/Depdency/ModelInstaller.cs +++ b/WPF/VMagicMirrorConfig/Depdency/ModelInstaller.cs @@ -25,6 +25,7 @@ public static void Initialize() resolver.Add(new ScreenshotTaker()); resolver.Add(new HotKeyModel()); resolver.Add(new FaceMotionBlendShapeNameStore()); + resolver.Add(new CustomMotionList()); resolver.Add(new GameInputSettingModel()); resolver.Add(new PreferenceSettingModel()); @@ -49,7 +50,6 @@ public static void Initialize() resolver.Add(new RuntimeHelper()); resolver.Add(new DeviceListSource()); - resolver.Add(new CustomMotionList()); resolver.Add(new ImageQualitySetting()); resolver.Add(new WordToMotionRuntimeConfig()); resolver.Add(new ExternalTrackerRuntimeConfig()); diff --git a/WPF/VMagicMirrorConfig/Model/Entity/GameInputSetting.cs b/WPF/VMagicMirrorConfig/Model/Entity/GameInputSetting.cs index adcdedd65..249542bd0 100644 --- a/WPF/VMagicMirrorConfig/Model/Entity/GameInputSetting.cs +++ b/WPF/VMagicMirrorConfig/Model/Entity/GameInputSetting.cs @@ -1,5 +1,7 @@ using System; +using System.Linq; using System.Windows.Input; +using Newtonsoft.Json; namespace Baku.VMagicMirrorConfig { @@ -22,7 +24,8 @@ public enum GameInputStickAction public enum GameInputButtonAction { - None, + Custom = -1, + None = 0, Jump, Crouch, Run, @@ -65,7 +68,24 @@ public enum GameInputMouseButton Middle, } + /// + /// のカスタムの詳細を記述するクラス + /// + public class GameInputCustomAction + { + //NOTE: string 1つなのにclass化するのはループとかマスクとかIKどうするとか設定したい可能性に配慮するため + public string CustomKey { get; set; } = ""; + } + public class KeyboardKeyWithGameInputCustomAction + { + public GameInputCustomAction CustomAction { get; set; } = new(); + public string KeyCode { get; set; } = ""; + + [JsonIgnore] + public GameInputActionKey CustomActionKey => GameInputActionKey.Custom(CustomAction.CustomKey); + } + //Entityに移動していいんでは public class GameInputGamepadKeyAssign { @@ -83,6 +103,43 @@ public class GameInputGamepadKeyAssign public GameInputButtonAction ButtonView { get; set; } public GameInputButtonAction ButtonMenu { get; set; } + public GameInputCustomAction CustomButtonA { get; set; } = new(); + public GameInputCustomAction CustomButtonB { get; set; } = new(); + public GameInputCustomAction CustomButtonX { get; set; } = new(); + public GameInputCustomAction CustomButtonY { get; set; } = new(); + + public GameInputCustomAction CustomButtonLButton { get; set; } = new(); + public GameInputCustomAction CustomButtonLTrigger { get; set; } = new(); + public GameInputCustomAction CustomButtonRButton { get; set; } = new(); + public GameInputCustomAction CustomButtonRTrigger { get; set; } = new(); + + public GameInputCustomAction CustomButtonView { get; set; } = new(); + public GameInputCustomAction CustomButtonMenu { get; set; } = new(); + + [JsonIgnore] + public GameInputActionKey ButtonAKey => new(ButtonA, CustomButtonA); + [JsonIgnore] + public GameInputActionKey ButtonBKey => new(ButtonB, CustomButtonB); + [JsonIgnore] + public GameInputActionKey ButtonXKey => new(ButtonX, CustomButtonX); + [JsonIgnore] + public GameInputActionKey ButtonYKey => new(ButtonY, CustomButtonY); + + [JsonIgnore] + public GameInputActionKey ButtonLButtonKey => new(ButtonLButton, CustomButtonLButton); + [JsonIgnore] + public GameInputActionKey ButtonLTriggerKey => new(ButtonLTrigger, CustomButtonLTrigger); + [JsonIgnore] + public GameInputActionKey ButtonRButtonKey => new(ButtonRButton, CustomButtonRButton); + [JsonIgnore] + public GameInputActionKey ButtonRTriggerKey => new(ButtonRTrigger, CustomButtonRTrigger); + + [JsonIgnore] + public GameInputActionKey ButtonViewKey => new(ButtonView, CustomButtonView); + [JsonIgnore] + public GameInputActionKey ButtonMenuKey => new(ButtonMenu, CustomButtonMenu); + + public GameInputStickAction DPadLeft { get; set; } public GameInputStickAction StickLeft { get; set; } = GameInputStickAction.Move; public GameInputStickAction StickRight { get; set; } = GameInputStickAction.LookAround; @@ -97,6 +154,16 @@ public class GameInputKeyboardKeyAssign public GameInputButtonAction LeftClick { get; set; } public GameInputButtonAction RightClick { get; set; } public GameInputButtonAction MiddleClick { get; set; } + public GameInputCustomAction CustomLeftClick { get; set; } = new(); + public GameInputCustomAction CustomRightClick { get; set; } = new(); + public GameInputCustomAction CustomMiddleClick { get; set; } = new(); + [JsonIgnore] + public GameInputActionKey LeftClickKey => new(LeftClick, CustomLeftClick); + [JsonIgnore] + public GameInputActionKey RightClickKey => new(RightClick, CustomRightClick); + [JsonIgnore] + public GameInputActionKey MiddleClickKey => new(MiddleClick, CustomMiddleClick); + //よくあるやつなので + このキーアサインでは補助キーを無視したいのでShiftも特別扱い public bool UseWasdMove { get; set; } = true; @@ -112,6 +179,9 @@ public class GameInputKeyboardKeyAssign public string TriggerKeyCode { get; set; } = ""; public string PunchKeyCode { get; set; } = ""; + public KeyboardKeyWithGameInputCustomAction[] CustomActions { get; set; } + = Array.Empty(); + public static GameInputKeyboardKeyAssign LoadDefault() => new(); //Unityに投げつける用に前処理したデータを生成する @@ -134,6 +204,13 @@ public GameInputKeyboardKeyAssign GetKeyCodeTranslatedData() result.CrouchKeyCode = TranslateKeyCode(CrouchKeyCode); result.TriggerKeyCode = TranslateKeyCode(TriggerKeyCode); result.PunchKeyCode = TranslateKeyCode(PunchKeyCode); + result.CustomActions = CustomActions + .Select(a => new KeyboardKeyWithGameInputCustomAction() + { + KeyCode = TranslateKeyCode(a.KeyCode), + CustomAction = a.CustomAction, + }) + .ToArray(); return result; } @@ -168,8 +245,8 @@ public class GameInputSetting /// NOTE: 規約としてこの値は書き換えません。 /// デフォルト値を参照したい人が、プロパティ読み込みのみの為だけに使います。 /// - public static GameInputSetting Default { get; } = new(); - + public static GameInputSetting LoadDefault() => new(); + public bool GamepadEnabled { get; set; } = true; public bool KeyboardEnabled { get; set; } = true; public bool AlwaysRun { get; set; } = true; diff --git a/WPF/VMagicMirrorConfig/Model/GameInput/GameInputActionKey.cs b/WPF/VMagicMirrorConfig/Model/GameInput/GameInputActionKey.cs new file mode 100644 index 000000000..001f92ed7 --- /dev/null +++ b/WPF/VMagicMirrorConfig/Model/GameInput/GameInputActionKey.cs @@ -0,0 +1,46 @@ +using System; + +namespace Baku.VMagicMirrorConfig +{ + public readonly struct GameInputActionKey : IEquatable + { + + public GameInputActionKey(GameInputButtonAction actionType, GameInputCustomAction customAction) + { + ActionType = actionType; + CustomActionKey = actionType is GameInputButtonAction.Custom ? customAction.CustomKey : ""; + } + + private GameInputActionKey(GameInputButtonAction actionType, string key) + { + ActionType = actionType; + CustomActionKey = key; + } + + //NOTE: データの期待としてCustomじゃないActionに対するCustomAction.CustomKeyは必ず空…というのを想定している + public GameInputButtonAction ActionType { get; } + public string CustomActionKey { get; } + public GameInputCustomAction CustomAction => new() { CustomKey = CustomActionKey }; + + + public bool Equals(GameInputActionKey other) + { + return + ActionType == other.ActionType && + CustomActionKey == other.CustomActionKey; + } + + public override bool Equals(object? obj) => obj is GameInputActionKey other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(ActionType, CustomActionKey); + + public override string ToString() => $"ActionKey:{ActionType}{(ActionType is GameInputButtonAction.Custom ? "-" + CustomActionKey : "")}"; + + public static GameInputActionKey BuiltIn(GameInputButtonAction action) + => new(action, new GameInputCustomAction()); + public static GameInputActionKey Custom(string key) + => new(GameInputButtonAction.Custom, key); + + public static GameInputActionKey Empty { get; } = new(GameInputButtonAction.None, ""); + } +} diff --git a/WPF/VMagicMirrorConfig/Model/InterProcess/Core/MessageFactory.cs b/WPF/VMagicMirrorConfig/Model/InterProcess/Core/MessageFactory.cs index 57d863293..9c97ca64d 100644 --- a/WPF/VMagicMirrorConfig/Model/InterProcess/Core/MessageFactory.cs +++ b/WPF/VMagicMirrorConfig/Model/InterProcess/Core/MessageFactory.cs @@ -307,10 +307,10 @@ private static Message WithArg(int content, [CallerMemberName] string command = public Message RequestCustomMotionDoctor() => NoArg(); /// - /// Query + /// Query : 引数をtrueにすると .vrma 形式になってるものだけ返却してくれる /// /// - public Message GetAvailableCustomMotionClipNames() => NoArg(); + public Message GetAvailableCustomMotionClipNames(bool vrmaOnly) => WithArg(vrmaOnly); #endregion diff --git a/WPF/VMagicMirrorConfig/Model/InterProcess/Util/CustomMotionList.cs b/WPF/VMagicMirrorConfig/Model/InterProcess/Util/CustomMotionList.cs index 9f110409d..4b33bd2e9 100644 --- a/WPF/VMagicMirrorConfig/Model/InterProcess/Util/CustomMotionList.cs +++ b/WPF/VMagicMirrorConfig/Model/InterProcess/Util/CustomMotionList.cs @@ -12,25 +12,52 @@ public CustomMotionList(IMessageSender sender) { _sender = sender; CustomMotionClipNames = new ReadOnlyObservableCollection(_customMotionClipNames); + VrmaCustomMotionClipNames = new ReadOnlyObservableCollection(_vrmaCustomMotionClipNames); } private readonly IMessageSender _sender; - private readonly ObservableCollection _customMotionClipNames = new ObservableCollection(); + private readonly ObservableCollection _customMotionClipNames = new(); public ReadOnlyObservableCollection CustomMotionClipNames { get; } + private readonly ObservableCollection _vrmaCustomMotionClipNames = new(); + public ReadOnlyObservableCollection VrmaCustomMotionClipNames { get; } + + private readonly TaskCompletionSource _initializeTcs = new(); + + public async Task WaitCustomMotionInitializeAsync() => await _initializeTcs.Task; + + private readonly object _isInitializedLock = new(); + private bool _isInitialized = false; + public bool IsInitialized + { + get { lock (_isInitializedLock) return _isInitialized; } + private set { lock (_isInitializedLock) _isInitialized = value; } + } + public async Task InitializeCustomMotionClipNamesAsync() { - var clipNames = await GetAvailableCustomMotionClipNamesAsync(); + //NOTE: 2回取得するのは若干もっさりするが、アプリ起動後の1回だけなので許容しておく + var clipNames = await GetCustomMotionClipNamesAsync(false); foreach (var name in clipNames) { _customMotionClipNames.Add(name); } + + var vrmaClipNames = await GetCustomMotionClipNamesAsync(true); + foreach (var name in vrmaClipNames) + { + _vrmaCustomMotionClipNames.Add(name); + } + + IsInitialized = true; + _initializeTcs.SetResult(true); } - private async Task GetAvailableCustomMotionClipNamesAsync() + private async Task GetCustomMotionClipNamesAsync(bool vrmaOnly) { - var rawClipNames = await _sender.QueryMessageAsync(MessageFactory.Instance.GetAvailableCustomMotionClipNames()); + var rawClipNames = await _sender.QueryMessageAsync( + MessageFactory.Instance.GetAvailableCustomMotionClipNames(vrmaOnly)); return rawClipNames.Split('\t'); } } diff --git a/WPF/VMagicMirrorConfig/Model/SettingModel/GameInputSettingModel.cs b/WPF/VMagicMirrorConfig/Model/SettingModel/GameInputSettingModel.cs index 9713839f6..a3fb6a63d 100644 --- a/WPF/VMagicMirrorConfig/Model/SettingModel/GameInputSettingModel.cs +++ b/WPF/VMagicMirrorConfig/Model/SettingModel/GameInputSettingModel.cs @@ -1,21 +1,27 @@ using Newtonsoft.Json; using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; namespace Baku.VMagicMirrorConfig { class GameInputSettingModel { - public GameInputSettingModel() : this(ModelResolver.Instance.Resolve()) + public GameInputSettingModel() : this( + ModelResolver.Instance.Resolve(), + ModelResolver.Instance.Resolve() + ) { } - public GameInputSettingModel(IMessageSender sender) + public GameInputSettingModel(IMessageSender sender, CustomMotionList customMotionList) { _sender = sender; - var setting = GameInputSetting.Default; - + _motionList = customMotionList; + var setting = GameInputSetting.LoadDefault(); + GamepadKeyAssign = setting.GamepadKeyAssign; KeyboardKeyAssign = setting.KeyboardKeyAssign; @@ -59,11 +65,12 @@ public GameInputSettingModel(IMessageSender sender) KeyboardKeyAssign.UseSpaceJump = v; SendMessage(factory.UseSpaceJumpGameInput(v)); }); - } private readonly IMessageSender _sender; - + private readonly CustomMotionList _motionList; + private GameInputActionKey[]? _customActionKeys = null; + public event EventHandler? GamepadKeyAssignUpdated; public event EventHandler? KeyboardKeyAssignUpdated; @@ -81,9 +88,6 @@ public GameInputSettingModel(IMessageSender sender) public RProperty UseShiftRun { get; } public RProperty UseSpaceJump { get; } - public static string GetSettingFolderPath() => SpecialFilePath.SaveFileDir; - public static string GetFileIoExt() => SpecialFilePath.GameInputSettingFileExt; - public void LoadSetting(string filePath) { if (!File.Exists(filePath)) @@ -117,6 +121,13 @@ public void LoadSetting(string filePath) public void SaveSetting(string filePath) { + //NOTE: 起動直後にアプリケーションが終了する場合ここを通る + //たぶん問題ないはずだが、カスタムモーションの登録状況に責任を持ちにくいので止めておく感じにしている + if (!_motionList.IsInitialized) + { + return; + } + var serializer = new JsonSerializer(); var sb = new StringBuilder(); @@ -134,44 +145,84 @@ public void SaveSetting(string filePath) } } - public void LoadSettingFromDefaultFile() => LoadSetting(SpecialFilePath.GameInputDefaultFilePath); + public async void InitializeAsync() + { + LoadSetting(SpecialFilePath.GameInputDefaultFilePath); + + // 設定ファイルの読み込みより後でカスタムモーションの一覧が来るとカスタムモーション一覧の整合性チェックをしてない状態が作れてしまうので、 + // 明示的に待機 + チェックをやっておく + await _motionList.WaitCustomMotionInitializeAsync(); + CheckCustomActionKeys(); + } + public void SaveSettingToDefaultFile() => SaveSetting(SpecialFilePath.GameInputDefaultFilePath); - public void SetGamepadButtonAction(GameInputGamepadButton button, GameInputButtonAction action) + public void SetGamepadButtonAction(GameInputGamepadButton button, GameInputActionKey actionKey) { - var current = button switch + var currentKey = button switch { - GameInputGamepadButton.A => GamepadKeyAssign.ButtonA, - GameInputGamepadButton.B => GamepadKeyAssign.ButtonB, - GameInputGamepadButton.X => GamepadKeyAssign.ButtonX, - GameInputGamepadButton.Y => GamepadKeyAssign.ButtonY, - GameInputGamepadButton.LB => GamepadKeyAssign.ButtonLButton, - GameInputGamepadButton.RB => GamepadKeyAssign.ButtonRButton, - GameInputGamepadButton.LTrigger => GamepadKeyAssign.ButtonLTrigger, - GameInputGamepadButton.RTrigger => GamepadKeyAssign.ButtonRTrigger, - GameInputGamepadButton.View => GamepadKeyAssign.ButtonView, - GameInputGamepadButton.Menu => GamepadKeyAssign.ButtonMenu, - _ => GameInputButtonAction.None, + GameInputGamepadButton.A => GamepadKeyAssign.ButtonAKey, + GameInputGamepadButton.B => GamepadKeyAssign.ButtonBKey, + GameInputGamepadButton.X => GamepadKeyAssign.ButtonXKey, + GameInputGamepadButton.Y => GamepadKeyAssign.ButtonYKey, + GameInputGamepadButton.LB => GamepadKeyAssign.ButtonLButtonKey, + GameInputGamepadButton.RB => GamepadKeyAssign.ButtonRButtonKey, + GameInputGamepadButton.LTrigger => GamepadKeyAssign.ButtonLTriggerKey, + GameInputGamepadButton.RTrigger => GamepadKeyAssign.ButtonRTriggerKey, + GameInputGamepadButton.View => GamepadKeyAssign.ButtonViewKey, + GameInputGamepadButton.Menu => GamepadKeyAssign.ButtonMenuKey, + _ => GameInputActionKey.Empty, }; - if (action == current) + if (actionKey.Equals(currentKey)) { return; } switch(button) { - case GameInputGamepadButton.A: GamepadKeyAssign.ButtonA = action; break; - case GameInputGamepadButton.B: GamepadKeyAssign.ButtonB = action; break; - case GameInputGamepadButton.X: GamepadKeyAssign.ButtonX = action; break; - case GameInputGamepadButton.Y: GamepadKeyAssign.ButtonY = action; break; - case GameInputGamepadButton.LB: GamepadKeyAssign.ButtonLButton = action; break; - case GameInputGamepadButton.RB: GamepadKeyAssign.ButtonRButton = action; break; - case GameInputGamepadButton.LTrigger: GamepadKeyAssign.ButtonLTrigger = action; break; - case GameInputGamepadButton.RTrigger: GamepadKeyAssign.ButtonRTrigger = action; break; - case GameInputGamepadButton.View: GamepadKeyAssign.ButtonView = action; break; - case GameInputGamepadButton.Menu: GamepadKeyAssign.ButtonMenu = action; break; - default: return; + case GameInputGamepadButton.A: + GamepadKeyAssign.ButtonA = actionKey.ActionType; + GamepadKeyAssign.CustomButtonA = actionKey.CustomAction; + break; + case GameInputGamepadButton.B: + GamepadKeyAssign.ButtonB = actionKey.ActionType; + GamepadKeyAssign.CustomButtonB = actionKey.CustomAction; + break; + case GameInputGamepadButton.X: + GamepadKeyAssign.ButtonX = actionKey.ActionType; + GamepadKeyAssign.CustomButtonX = actionKey.CustomAction; + break; + case GameInputGamepadButton.Y: + GamepadKeyAssign.ButtonY = actionKey.ActionType; + GamepadKeyAssign.CustomButtonY = actionKey.CustomAction; + break; + case GameInputGamepadButton.LB: + GamepadKeyAssign.ButtonLButton = actionKey.ActionType; + GamepadKeyAssign.CustomButtonLButton = actionKey.CustomAction; + break; + case GameInputGamepadButton.RB: + GamepadKeyAssign.ButtonRButton = actionKey.ActionType; + GamepadKeyAssign.CustomButtonRButton = actionKey.CustomAction; + break; + case GameInputGamepadButton.LTrigger: + GamepadKeyAssign.ButtonLTrigger = actionKey.ActionType; + GamepadKeyAssign.CustomButtonLTrigger = actionKey.CustomAction; + break; + case GameInputGamepadButton.RTrigger: + GamepadKeyAssign.ButtonRTrigger = actionKey.ActionType; + GamepadKeyAssign.CustomButtonRTrigger = actionKey.CustomAction; + break; + case GameInputGamepadButton.View: + GamepadKeyAssign.ButtonView = actionKey.ActionType; + GamepadKeyAssign.CustomButtonView = actionKey.CustomAction; + break; + case GameInputGamepadButton.Menu: + GamepadKeyAssign.ButtonMenu = actionKey.ActionType; + GamepadKeyAssign.CustomButtonMenu = actionKey.CustomAction; + break; + default: + return; } SendGamepadKeyAssign(); @@ -205,42 +256,52 @@ public void SetGamepadStickAction(GameInputGamepadStick stick, GameInputStickAct GamepadKeyAssignUpdated?.Invoke(this, new GamepadKeyAssignUpdateEventArgs(GamepadKeyAssign)); } - public void SetClickAction(GameInputMouseButton button, GameInputButtonAction action) + public void SetClickAction(GameInputMouseButton button, GameInputActionKey actionKey) { var current = button switch { - GameInputMouseButton.Left => KeyboardKeyAssign.LeftClick, - GameInputMouseButton.Right => KeyboardKeyAssign.RightClick, - GameInputMouseButton.Middle => KeyboardKeyAssign.MiddleClick, - _ => GameInputButtonAction.None, + GameInputMouseButton.Left => KeyboardKeyAssign.LeftClickKey, + GameInputMouseButton.Right => KeyboardKeyAssign.RightClickKey, + GameInputMouseButton.Middle => KeyboardKeyAssign.MiddleClickKey, + _ => GameInputActionKey.Empty, }; - if (action == current) + if (actionKey.Equals(current)) { return; } switch (button) { - case GameInputMouseButton.Left: KeyboardKeyAssign.LeftClick = action; break; - case GameInputMouseButton.Right: KeyboardKeyAssign.RightClick = action; break; - case GameInputMouseButton.Middle: KeyboardKeyAssign.MiddleClick = action; break; + case GameInputMouseButton.Left: + KeyboardKeyAssign.LeftClick = actionKey.ActionType; + KeyboardKeyAssign.CustomLeftClick = actionKey.CustomAction; + break; + case GameInputMouseButton.Right: + KeyboardKeyAssign.RightClick = actionKey.ActionType; + KeyboardKeyAssign.CustomRightClick = actionKey.CustomAction; + break; + case GameInputMouseButton.Middle: + KeyboardKeyAssign.MiddleClick = actionKey.ActionType; + KeyboardKeyAssign.CustomMiddleClick = actionKey.CustomAction; + break; default: return; } SendKeyboardKeyAssign(); - KeyboardKeyAssignUpdated?.Invoke(this, new KeyboardKeyAssignUpdateEventArgs(KeyboardKeyAssign)); + KeyboardKeyAssignUpdated?.Invoke(this, new(KeyboardKeyAssign)); } - public void SetKeyAction(GameInputButtonAction action, string key) + public void SetKeyAction(GameInputActionKey actionKey, string key) { - var current = action switch + var current = actionKey.ActionType switch { GameInputButtonAction.Jump => KeyboardKeyAssign.JumpKeyCode, GameInputButtonAction.Crouch => KeyboardKeyAssign.CrouchKeyCode, GameInputButtonAction.Run => KeyboardKeyAssign.RunKeyCode, GameInputButtonAction.Trigger => KeyboardKeyAssign.TriggerKeyCode, GameInputButtonAction.Punch => KeyboardKeyAssign.PunchKeyCode, + GameInputButtonAction.Custom => FindKeyCodeOfCustomAction(actionKey), _ => "", }; @@ -249,21 +310,128 @@ public void SetKeyAction(GameInputButtonAction action, string key) return; } - switch (action) + switch (actionKey.ActionType) { case GameInputButtonAction.Jump: KeyboardKeyAssign.JumpKeyCode = key; break; case GameInputButtonAction.Crouch: KeyboardKeyAssign.CrouchKeyCode = key; break; case GameInputButtonAction.Run: KeyboardKeyAssign.RunKeyCode = key; break; case GameInputButtonAction.Trigger: KeyboardKeyAssign.TriggerKeyCode = key; break; case GameInputButtonAction.Punch: KeyboardKeyAssign.PunchKeyCode = key; break; + case GameInputButtonAction.Custom: + var target = KeyboardKeyAssign + .CustomActions + .FirstOrDefault(a => a.CustomAction.CustomKey == actionKey.CustomActionKey); + if (target == null) + { + return; + } + + target.KeyCode = key; + break; default: return; } SendKeyboardKeyAssign(); - KeyboardKeyAssignUpdated?.Invoke(this, new KeyboardKeyAssignUpdateEventArgs(KeyboardKeyAssign)); + KeyboardKeyAssignUpdated?.Invoke(this, new(KeyboardKeyAssign)); } - public void ResetToDefault() => ApplySetting(GameInputSetting.Default); + //Unity側からカスタムモーション一覧を受け取ったより後でこの関数が呼ばれると、 + //カスタムアクション用のキーボード設定に過不足があった場合の内容が修正される。 + // - 指定した一覧にはあるのに設定として保持してない -> キーアサインがない状態で追加 + // - 指定した一覧に入ってないものが設定に含まれる -> 削除 + private void CheckCustomActionKeys() + { + // Unityから一覧を受け取る前だと一致チェックできないので、修正を試みない + if (!_motionList.IsInitialized) + { + return; + } + + var actionKeys = LoadCustomActionKeys(); + + var currentKeys = KeyboardKeyAssign + .CustomActions + .Select(a => GameInputActionKey.Custom(a.CustomAction.CustomKey)) + .ToHashSet(); + + if (currentKeys.SetEquals(actionKeys)) + { + return; + } + + var resultCustomActions = new KeyboardKeyWithGameInputCustomAction[actionKeys.Length]; + for (var i = 0; i < resultCustomActions.Length; i++) + { + var key = actionKeys[i]; + resultCustomActions[i] = new KeyboardKeyWithGameInputCustomAction() + { + CustomAction = new GameInputCustomAction() { CustomKey = key.CustomActionKey }, + KeyCode = FindKeyCodeOfCustomAction(key), + }; + } + KeyboardKeyAssign.CustomActions = resultCustomActions.ToArray(); + + SendKeyboardKeyAssign(); + KeyboardKeyAssignUpdated?.Invoke(this, new(KeyboardKeyAssign)); + } + + + public void ResetToDefault() => ApplySetting(GameInputSetting.LoadDefault()); + + /// + /// NOTE: BuiltInアクションに対しても定義できるが、直近で必要ないため使っていない + /// + /// + /// + public string FindKeyCodeOfCustomAction(GameInputActionKey action) + { + var item = KeyboardKeyAssign.CustomActions + .FirstOrDefault(a => a.CustomActionKey.Equals(action)); + + if (item != null) + { + return item.KeyCode; + } + else + { + return ""; + } + } + + public GameInputActionKey[] LoadCustomActionKeys() + { + if (!_motionList.IsInitialized) + { + return Array.Empty(); + } + + if (_customActionKeys == null) + { + _customActionKeys = _motionList.VrmaCustomMotionClipNames + .Select(GameInputActionKey.Custom) + .ToArray(); + } + return _customActionKeys; + } + + //NOTE: + // 起動直後でcustomMotionListが空の状態で呼ぶと .vrma を含まない結果が戻ってしまうが、 + // この問題は特にケアしない(ゲーム入力の設定ウィンドウが開くまでは呼ばれないはずなので) + public GameInputActionKey[] GetAvailableActionKeys() + { + var result = new List() + { + GameInputActionKey.BuiltIn(GameInputButtonAction.None), + GameInputActionKey.BuiltIn(GameInputButtonAction.Jump), + GameInputActionKey.BuiltIn(GameInputButtonAction.Crouch), + GameInputActionKey.BuiltIn(GameInputButtonAction.Run), + GameInputActionKey.BuiltIn(GameInputButtonAction.Trigger), + GameInputActionKey.BuiltIn(GameInputButtonAction.Punch), + }; + + result.AddRange(LoadCustomActionKeys()); + return result.ToArray(); + } private GameInputSetting BuildCurrentSetting() @@ -280,7 +448,7 @@ private GameInputSetting BuildCurrentSetting() }; } - void ApplySetting(GameInputSetting setting) + private void ApplySetting(GameInputSetting setting) { GamepadEnabled.Value = setting.GamepadEnabled; KeyboardEnabled.Value = setting.KeyboardEnabled; @@ -301,9 +469,11 @@ void ApplySetting(GameInputSetting setting) SendKeyboardKeyAssign(); GamepadKeyAssignUpdated?.Invoke(this, new(GamepadKeyAssign)); KeyboardKeyAssignUpdated?.Invoke(this, new(KeyboardKeyAssign)); + + CheckCustomActionKeys(); } - void SendGamepadKeyAssign() + private void SendGamepadKeyAssign() { var serializer = new JsonSerializer(); var sb = new StringBuilder(); diff --git a/WPF/VMagicMirrorConfig/Resources/English.xaml b/WPF/VMagicMirrorConfig/Resources/English.xaml index 7e3142a4d..347fb112b 100644 --- a/WPF/VMagicMirrorConfig/Resources/English.xaml +++ b/WPF/VMagicMirrorConfig/Resources/English.xaml @@ -609,7 +609,7 @@ VRoid Hub model is not supported in this feature. *When enabled, "Run" button works as "Walk" Always Aim - Keyboard: + Keyboard (Click to Hide) (None) Move Look Around @@ -626,7 +626,7 @@ VRoid Hub model is not supported in this feature. Use SPACE to jump Additional Key Assign: - Mouse: + Mouse (Click to Hide) Use mouse move to look around Left Click diff --git a/WPF/VMagicMirrorConfig/Resources/Japanese.xaml b/WPF/VMagicMirrorConfig/Resources/Japanese.xaml index f23ec26d5..3fc75fff6 100644 --- a/WPF/VMagicMirrorConfig/Resources/Japanese.xaml +++ b/WPF/VMagicMirrorConfig/Resources/Japanese.xaml @@ -622,14 +622,14 @@ VMCPを使わない場合、「設定タブをメインウィンドウから非 銃を撃つ パンチ - キーボード: + キーボード (クリックで折りたたみ) WASDで移動 矢印キーで移動 Shiftキーでダッシュ Spaceキーでジャンプ その他のキーアサイン: - マウス: + マウス (クリックで折りたたみ) マウス移動で見回し 左クリック diff --git a/WPF/VMagicMirrorConfig/View/Code/Converter/GameInputActionKeyToStringConverter.cs b/WPF/VMagicMirrorConfig/View/Code/Converter/GameInputActionKeyToStringConverter.cs new file mode 100644 index 000000000..1a6a27ce1 --- /dev/null +++ b/WPF/VMagicMirrorConfig/View/Code/Converter/GameInputActionKeyToStringConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace Baku.VMagicMirrorConfig.View +{ + public class GameInputActionKeyToStringConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not GameInputActionKey key) + { + return Binding.DoNothing; + } + + // - customの場合は翻訳されない + // - customの場合は"vrma_"みたいな接頭辞がついてるはずなので、ビルトインのものとは区別がつく想定 + switch (key.ActionType) + { + case GameInputButtonAction.None: return LocalizedString.GetString("GameInputKeyAssign_Action_None"); + case GameInputButtonAction.Jump: return LocalizedString.GetString("GameInputKeyAssign_ButtonAction_Jump"); + case GameInputButtonAction.Crouch: return LocalizedString.GetString("GameInputKeyAssign_ButtonAction_Crouch"); + case GameInputButtonAction.Run: return LocalizedString.GetString("GameInputKeyAssign_ButtonAction_Run"); + case GameInputButtonAction.Trigger: return LocalizedString.GetString("GameInputKeyAssign_ButtonAction_Trigger"); + case GameInputButtonAction.Punch: return LocalizedString.GetString("GameInputKeyAssign_ButtonAction_Punch"); + case GameInputButtonAction.Custom: return key.CustomActionKey; + } + + //通常ここには到達しない + return Binding.DoNothing; + } + + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => Binding.DoNothing; + } +} diff --git a/WPF/VMagicMirrorConfig/View/GameInput/GameInputKeyAssignItem.xaml b/WPF/VMagicMirrorConfig/View/GameInput/GameInputKeyAssignItem.xaml index 65724b520..d22e2ca2a 100644 --- a/WPF/VMagicMirrorConfig/View/GameInput/GameInputKeyAssignItem.xaml +++ b/WPF/VMagicMirrorConfig/View/GameInput/GameInputKeyAssignItem.xaml @@ -10,14 +10,19 @@ mc:Ignorable="d" d:DataContext="{d:DesignInstance vm:GameInputKeyAssignItemViewModel}" d:DesignHeight="40" d:DesignWidth="450"> + + + - + - - + + - + + @@ -203,31 +214,31 @@ /> @@ -286,42 +297,42 @@ /> @@ -344,97 +355,102 @@ Margin="10,10,5,0" IsChecked="{Binding KeyboardEnabled.Value, Mode=TwoWay}" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WPF/VMagicMirrorConfig/ViewModel/KeyAssign/GameInputKeyboardKeyAssignItemViewModel.cs b/WPF/VMagicMirrorConfig/ViewModel/KeyAssign/GameInputKeyboardKeyAssignItemViewModel.cs index bdc4449ac..346c76be0 100644 --- a/WPF/VMagicMirrorConfig/ViewModel/KeyAssign/GameInputKeyboardKeyAssignItemViewModel.cs +++ b/WPF/VMagicMirrorConfig/ViewModel/KeyAssign/GameInputKeyboardKeyAssignItemViewModel.cs @@ -1,19 +1,16 @@ using System; -using System.Linq; using System.Windows.Input; namespace Baku.VMagicMirrorConfig.ViewModel { public class GameInputKeyAssignItemViewModel { - public GameInputKeyAssignItemViewModel(GameInputButtonAction action, string key) + public GameInputKeyAssignItemViewModel(GameInputActionKey actionKey, string keyCode) { - _actionItem = GameInputButtonActionItemViewModel.AvailableItems.FirstOrDefault(item => item.Action == action) - ?? new GameInputButtonActionItemViewModel(GameInputButtonAction.None, ""); - + ActionKey = actionKey; KeyDownCommand = new ActionCommand(OnKeyDown); ClearInputCommand = new ActionCommand(ClearInput); - SetKey(key); + SetKey(keyCode); //入力文字列は常にカラに戻す。入力欄は単にキー入力を受けるためだけに使われる RegisteredKeyInput.PropertyChanged += (_, __) => @@ -25,9 +22,7 @@ public GameInputKeyAssignItemViewModel(GameInputButtonAction action, string key) //NOTE: nullになるのはアクションにキーが割当たってない状態 private Key? _key; - private readonly GameInputButtonActionItemViewModel _actionItem; - public GameInputButtonAction Action => _actionItem.Action; - public RProperty Label => _actionItem.Label; + public GameInputActionKey ActionKey { get; } public RProperty RegisteredKeyInput { get; } = new RProperty(""); public RProperty RegisteredKey { get; } = new RProperty(""); @@ -35,7 +30,7 @@ public GameInputKeyAssignItemViewModel(GameInputButtonAction action, string key) public ActionCommand KeyDownCommand { get; } public ActionCommand ClearInputCommand { get; } - public event Action? RegisteredKeyChanged; + public event Action<(GameInputActionKey key, string keyCode)>? RegisteredKeyChanged; public void SetKey(string key) { @@ -70,7 +65,7 @@ private void OnKeyDown(object? obj) //NOTE: (Left|Right)(Alt|Ctrl|Shift|Win)が入る事がある。はず。 _key = nextKey; RegisteredKey.Value = CreateRegisteredKeyString(); - RegisteredKeyChanged?.Invoke(_key?.ToString() ?? ""); + RegisteredKeyChanged?.Invoke((ActionKey, _key?.ToString() ?? "")); } //NOTE: 横着でこう書いてるが、別にOnKeyDownに帰着させないでも動くならOK diff --git a/WPF/VMagicMirrorConfig/ViewModel/KeyAssign/KeyAssignViewModel.cs b/WPF/VMagicMirrorConfig/ViewModel/KeyAssign/KeyAssignViewModel.cs index 34da9f556..ae4f9a0f3 100644 --- a/WPF/VMagicMirrorConfig/ViewModel/KeyAssign/KeyAssignViewModel.cs +++ b/WPF/VMagicMirrorConfig/ViewModel/KeyAssign/KeyAssignViewModel.cs @@ -1,6 +1,7 @@ using Microsoft.Win32; using System; -using System.ComponentModel; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Windows; @@ -21,28 +22,29 @@ internal KeyAssignViewModel(GameInputSettingModel model) var gamepad = IsInDesignMode ? new GameInputGamepadKeyAssign() : model.GamepadKeyAssign; var keyboard = IsInDesignMode ? new GameInputKeyboardKeyAssign() : model.KeyboardKeyAssign; - - ButtonA = new RProperty( - gamepad.ButtonA, a => SetGamepadButtonAction(GameInputGamepadButton.A, a)); - ButtonB = new RProperty( - gamepad.ButtonB, a => SetGamepadButtonAction(GameInputGamepadButton.B, a)); - ButtonX = new RProperty( - gamepad.ButtonX, a => SetGamepadButtonAction(GameInputGamepadButton.X, a)); - ButtonY = new RProperty( - gamepad.ButtonY, a => SetGamepadButtonAction(GameInputGamepadButton.Y, a)); - - ButtonRB = new RProperty( - gamepad.ButtonRButton, a => SetGamepadButtonAction(GameInputGamepadButton.RB, a)); - ButtonLB = new RProperty( - gamepad.ButtonLButton, a => SetGamepadButtonAction(GameInputGamepadButton.LB, a)); - ButtonRTrigger = new RProperty( - gamepad.ButtonRTrigger, a => SetGamepadButtonAction(GameInputGamepadButton.RTrigger, a)); - ButtonLTrigger = new RProperty( - gamepad.ButtonLTrigger, a => SetGamepadButtonAction(GameInputGamepadButton.LTrigger, a)); - ButtonView = new RProperty( - gamepad.ButtonView, a => SetGamepadButtonAction(GameInputGamepadButton.View, a)); - ButtonMenu = new RProperty( - gamepad.ButtonMenu, a => SetGamepadButtonAction(GameInputGamepadButton.Menu, a)); + ButtonActions = IsInDesignMode ? Array.Empty() : _model.GetAvailableActionKeys(); + + ButtonA = new RProperty( + gamepad.ButtonAKey, a => SetGamepadButtonAction(GameInputGamepadButton.A, a)); + ButtonB = new RProperty( + gamepad.ButtonBKey, a => SetGamepadButtonAction(GameInputGamepadButton.B, a)); + ButtonX = new RProperty( + gamepad.ButtonXKey, a => SetGamepadButtonAction(GameInputGamepadButton.X, a)); + ButtonY = new RProperty( + gamepad.ButtonYKey, a => SetGamepadButtonAction(GameInputGamepadButton.Y, a)); + + ButtonRB = new RProperty( + gamepad.ButtonRButtonKey, a => SetGamepadButtonAction(GameInputGamepadButton.RB, a)); + ButtonLB = new RProperty( + gamepad.ButtonLButtonKey, a => SetGamepadButtonAction(GameInputGamepadButton.LB, a)); + ButtonRTrigger = new RProperty( + gamepad.ButtonRTriggerKey, a => SetGamepadButtonAction(GameInputGamepadButton.RTrigger, a)); + ButtonLTrigger = new RProperty( + gamepad.ButtonLTriggerKey, a => SetGamepadButtonAction(GameInputGamepadButton.LTrigger, a)); + ButtonView = new RProperty( + gamepad.ButtonViewKey, a => SetGamepadButtonAction(GameInputGamepadButton.View, a)); + ButtonMenu = new RProperty( + gamepad.ButtonMenuKey, a => SetGamepadButtonAction(GameInputGamepadButton.Menu, a)); StickLeft = new RProperty( gamepad.StickLeft, a => SetGamepadStickAction(GameInputGamepadStick.Left, a)); @@ -52,12 +54,12 @@ internal KeyAssignViewModel(GameInputSettingModel model) gamepad.DPadLeft, a => SetGamepadStickAction(GameInputGamepadStick.DPadLeft, a)); - LeftClick = new RProperty( - keyboard.LeftClick, a => SetMouseClickAction(GameInputMouseButton.Left, a)); - RightClick = new RProperty( - keyboard.RightClick, a => SetMouseClickAction(GameInputMouseButton.Right, a)); - MiddleClick = new RProperty( - keyboard.MiddleClick, a => SetMouseClickAction(GameInputMouseButton.Middle, a)); + LeftClick = new RProperty( + keyboard.LeftClickKey, a => SetMouseClickAction(GameInputMouseButton.Left, a)); + RightClick = new RProperty( + keyboard.RightClickKey, a => SetMouseClickAction(GameInputMouseButton.Right, a)); + MiddleClick = new RProperty( + keyboard.MiddleClickKey, a => SetMouseClickAction(GameInputMouseButton.Middle, a)); ResetSettingsCommand = new ActionCommand(ResetSetting); LoadSettingFileCommand = new ActionCommand(LoadSetting); @@ -67,70 +69,86 @@ internal KeyAssignViewModel(GameInputSettingModel model) () => UrlNavigate.Open(LocalizedString.GetString("URL_docs_game_input")) ); - KeyAssigns = Array.Empty(); + if (IsInDesignMode) + { + return; + } + + WeakEventManager.AddHandler( + _model, + nameof(_model.GamepadKeyAssignUpdated), + OnGamepadKeyAssignUpdated + ); - if (!IsInDesignMode) + WeakEventManager.AddHandler( + _model, + nameof(_model.KeyboardKeyAssignUpdated), + OnKeyboardKeyAssignUpdated + ); + + var keyAssignViewModels = new List(); + keyAssignViewModels.AddRange( + new (GameInputButtonAction action, string keyCode)[] + { + (GameInputButtonAction.Jump,_model.KeyboardKeyAssign.JumpKeyCode), + (GameInputButtonAction.Crouch, _model.KeyboardKeyAssign.CrouchKeyCode), + (GameInputButtonAction.Run, _model.KeyboardKeyAssign.RunKeyCode), + (GameInputButtonAction.Trigger, _model.KeyboardKeyAssign.TriggerKeyCode), + (GameInputButtonAction.Punch, _model.KeyboardKeyAssign.PunchKeyCode), + }.Select(pair => CreateKeyAssignItemViewModel( + GameInputActionKey.BuiltIn(pair.action), + pair.keyCode + ) + )); + + keyAssignViewModels.AddRange(_model + .LoadCustomActionKeys() + .Select(actionKey => CreateKeyAssignItemViewModel( + actionKey, + _model.FindKeyCodeOfCustomAction(actionKey) + ) + )); + + + foreach (var keyAssign in keyAssignViewModels) { - WeakEventManager.AddHandler( - _model, - nameof(_model.GamepadKeyAssignUpdated), - OnGamepadKeyAssignUpdated - ); - - WeakEventManager.AddHandler( - _model, - nameof(_model.KeyboardKeyAssignUpdated), - OnKeyboardKeyAssignUpdated - ); - - KeyAssigns = new (GameInputButtonAction action, string keyCode)[] - { - (GameInputButtonAction.Jump,_model.KeyboardKeyAssign.JumpKeyCode), - (GameInputButtonAction.Crouch, _model.KeyboardKeyAssign.CrouchKeyCode), - (GameInputButtonAction.Run, _model.KeyboardKeyAssign.RunKeyCode), - (GameInputButtonAction.Trigger, _model.KeyboardKeyAssign.TriggerKeyCode), - (GameInputButtonAction.Punch, _model.KeyboardKeyAssign.PunchKeyCode), - }.Select(pair => - { - var vm = new GameInputKeyAssignItemViewModel(pair.action, pair.keyCode); - vm.RegisteredKeyChanged += key => _model.SetKeyAction(pair.action, key); - return vm; - }) - .ToArray(); + KeyAssigns.Add(keyAssign); } } private readonly GameInputSettingModel _model; private bool _silentMode = false; + #region Properties + public RProperty GamepadEnabled => _model.GamepadEnabled; public RProperty KeyboardEnabled => _model.KeyboardEnabled; public RProperty AlwaysRun => _model.AlwaysRun; public RProperty LocomotionStyle => _model.LocomotionStyle; - public RProperty ButtonA { get; } - public RProperty ButtonB { get; } - public RProperty ButtonX { get; } - public RProperty ButtonY { get; } + public RProperty ButtonA { get; } + public RProperty ButtonB { get; } + public RProperty ButtonX { get; } + public RProperty ButtonY { get; } //NOTE: LTriggerはボタンと連続値どっちがいいの、みたいな話もある - public RProperty ButtonLB { get; } - public RProperty ButtonLTrigger { get; } - public RProperty ButtonRB { get; } - public RProperty ButtonRTrigger { get; } + public RProperty ButtonLB { get; } + public RProperty ButtonLTrigger { get; } + public RProperty ButtonRB { get; } + public RProperty ButtonRTrigger { get; } - public RProperty ButtonView { get; } - public RProperty ButtonMenu { get; } + public RProperty ButtonView { get; } + public RProperty ButtonMenu { get; } public RProperty DPadLeft { get; } public RProperty StickLeft { get; } public RProperty StickRight { get; } - public RProperty LeftClick { get; } - public RProperty RightClick { get; } - public RProperty MiddleClick { get; } + public RProperty LeftClick { get; } + public RProperty RightClick { get; } + public RProperty MiddleClick { get; } public RProperty UseMouseToLookAround => _model.UseMouseToLookAround; @@ -144,19 +162,23 @@ internal KeyAssignViewModel(GameInputSettingModel model) public ActionCommand LoadSettingFileCommand { get; } public ActionCommand OpenDocUrlCommand { get; } - public GameInputKeyAssignItemViewModel[] KeyAssigns { get; } + public ObservableCollection KeyAssigns { get; } = new(); public GameInputLocomotionStyleViewModel[] LocomotionStyles => GameInputLocomotionStyleViewModel.AvailableItems; public GameInputStickActionItemViewModel[] StickActions => GameInputStickActionItemViewModel.AvailableItems; - public GameInputButtonActionItemViewModel[] ButtonActions => GameInputButtonActionItemViewModel.AvailableItems; - private void SetGamepadButtonAction(GameInputGamepadButton button, GameInputButtonAction action) + public GameInputActionKey[] ButtonActions { get; } + + #endregion + + + private void SetGamepadButtonAction(GameInputGamepadButton button, GameInputActionKey actionKey) { if (_silentMode) { return; } - _model.SetGamepadButtonAction(button, action); + _model.SetGamepadButtonAction(button, actionKey); } public void SetGamepadStickAction(GameInputGamepadStick stick, GameInputStickAction action) @@ -168,13 +190,13 @@ public void SetGamepadStickAction(GameInputGamepadStick stick, GameInputStickAct _model.SetGamepadStickAction(stick, action); } - public void SetMouseClickAction(GameInputMouseButton button, GameInputButtonAction action) + public void SetMouseClickAction(GameInputMouseButton button, GameInputActionKey actionKey) { if (_silentMode) { return; } - _model.SetClickAction(button, action); + _model.SetClickAction(button, actionKey); } private void OnGamepadKeyAssignUpdated(object? sender, GamepadKeyAssignUpdateEventArgs e) @@ -187,18 +209,18 @@ private void UpdateGamepadKeyAssign(GameInputGamepadKeyAssign data) _silentMode = true; try { - ButtonA.Value = data.ButtonA; - ButtonB.Value = data.ButtonB; - ButtonX.Value = data.ButtonX; - ButtonY.Value = data.ButtonY; + ButtonA.Value = data.ButtonAKey; + ButtonB.Value = data.ButtonBKey; + ButtonX.Value = data.ButtonXKey; + ButtonY.Value = data.ButtonYKey; - ButtonLB.Value = data.ButtonLButton; - ButtonRB.Value = data.ButtonRButton; - ButtonLTrigger.Value = data.ButtonLTrigger; - ButtonRTrigger.Value = data.ButtonRTrigger; + ButtonLB.Value = data.ButtonLButtonKey; + ButtonRB.Value = data.ButtonRButtonKey; + ButtonLTrigger.Value = data.ButtonLTriggerKey; + ButtonRTrigger.Value = data.ButtonRTriggerKey; - ButtonView.Value = data.ButtonView; - ButtonMenu.Value = data.ButtonMenu; + ButtonView.Value = data.ButtonViewKey; + ButtonMenu.Value = data.ButtonMenuKey; StickLeft.Value = data.StickLeft; StickRight.Value = data.StickRight; @@ -215,15 +237,43 @@ private void UpdateKeyboardKeyAssign(GameInputKeyboardKeyAssign data) _silentMode = true; try { - LeftClick.Value = data.LeftClick; - RightClick.Value = data.RightClick; - MiddleClick.Value = data.MiddleClick; - - KeyAssigns.FirstOrDefault(a => a.Action == GameInputButtonAction.Jump)?.SetKey(data.JumpKeyCode); - KeyAssigns.FirstOrDefault(a => a.Action == GameInputButtonAction.Crouch)?.SetKey(data.CrouchKeyCode); - KeyAssigns.FirstOrDefault(a => a.Action == GameInputButtonAction.Run)?.SetKey(data.RunKeyCode); - KeyAssigns.FirstOrDefault(a => a.Action == GameInputButtonAction.Trigger)?.SetKey(data.TriggerKeyCode); - KeyAssigns.FirstOrDefault(a => a.Action == GameInputButtonAction.Punch)?.SetKey(data.PunchKeyCode); + LeftClick.Value = data.LeftClickKey; + RightClick.Value = data.RightClickKey; + MiddleClick.Value = data.MiddleClickKey; + + KeyAssigns.FirstOrDefault(a => a.ActionKey.ActionType == GameInputButtonAction.Jump)?.SetKey(data.JumpKeyCode); + KeyAssigns.FirstOrDefault(a => a.ActionKey.ActionType == GameInputButtonAction.Crouch)?.SetKey(data.CrouchKeyCode); + KeyAssigns.FirstOrDefault(a => a.ActionKey.ActionType == GameInputButtonAction.Run)?.SetKey(data.RunKeyCode); + KeyAssigns.FirstOrDefault(a => a.ActionKey.ActionType == GameInputButtonAction.Trigger)?.SetKey(data.TriggerKeyCode); + KeyAssigns.FirstOrDefault(a => a.ActionKey.ActionType == GameInputButtonAction.Punch)?.SetKey(data.PunchKeyCode); + + foreach(var customAction in data.CustomActions) + { + var customKey = GameInputActionKey.Custom(customAction.CustomAction.CustomKey); + var assignedItem = KeyAssigns.FirstOrDefault(a => a.ActionKey.Equals(customKey)); + if (assignedItem == null) + { + assignedItem = CreateKeyAssignItemViewModel(customKey, customAction.KeyCode); + KeyAssigns.Add(assignedItem); + } + else + { + assignedItem.SetKey(customAction.KeyCode); + } + } + + //Model側が持ってないカスタムモーションのぶんを削除 + //TODO: もうちょいカッコよく書きたい… + var removeTarget = KeyAssigns + .Where(k => + k.ActionKey.ActionType is GameInputButtonAction.Custom && + !data.CustomActions.Any(a => a.CustomAction.CustomKey == k.ActionKey.CustomActionKey) + ) + .ToArray(); + foreach( var item in removeTarget) + { + DisposeKeyAssignItemViewModel(item); + } } finally { @@ -266,6 +316,22 @@ private void LoadSetting() } } + private GameInputKeyAssignItemViewModel CreateKeyAssignItemViewModel(GameInputActionKey actionKey, string keyCode) + { + var vm = new GameInputKeyAssignItemViewModel(actionKey, keyCode); + vm.RegisteredKeyChanged += OnRegisteredKeyChanged; + return vm; + } + + private void DisposeKeyAssignItemViewModel(GameInputKeyAssignItemViewModel vm) + { + vm.RegisteredKeyChanged -= OnRegisteredKeyChanged; + KeyAssigns.Remove(vm); + } + + private void OnRegisteredKeyChanged((GameInputActionKey actionKey, string keyCode) value) + => _model.SetKeyAction(value.actionKey, value.keyCode); + private void ResetSetting() { SettingResetUtils.ResetSingleCategoryAsync(() => _model.ResetToDefault()); @@ -326,37 +392,4 @@ public GameInputStickActionItemViewModel(GameInputStickAction action, string loc new (GameInputStickAction.LookAround, "GameInputKeyAssign_StickAction_LookAround"), }; } - - /// - /// ゲーム入力のうちスティックで取れるアクションを定義したもの - /// - public class GameInputButtonActionItemViewModel - { - public GameInputButtonActionItemViewModel(GameInputButtonAction action, string localizationKey) - { - Action = action; - _localizationKey = localizationKey; - if (!string.IsNullOrEmpty(_localizationKey)) - { - Label.Value = LocalizedString.GetString(_localizationKey); - LanguageSelector.Instance.LanguageChanged += - () => Label.Value = LocalizedString.GetString(_localizationKey); - } - } - - private readonly string _localizationKey; - public GameInputButtonAction Action { get; } - public RProperty Label { get; } = new RProperty(""); - - //NOTE: immutable arrayのほうが性質は良いのでそうしてもよい - public static GameInputButtonActionItemViewModel[] AvailableItems { get; } = new GameInputButtonActionItemViewModel[] - { - new (GameInputButtonAction.None, "GameInputKeyAssign_Action_None"), - new (GameInputButtonAction.Jump, "GameInputKeyAssign_ButtonAction_Jump"), - new (GameInputButtonAction.Crouch, "GameInputKeyAssign_ButtonAction_Crouch"), - new (GameInputButtonAction.Run, "GameInputKeyAssign_ButtonAction_Run"), - new (GameInputButtonAction.Trigger, "GameInputKeyAssign_ButtonAction_Trigger"), - new (GameInputButtonAction.Punch, "GameInputKeyAssign_ButtonAction_Punch"), - }; - } } diff --git a/WPF/VMagicMirrorConfig/ViewModel/MainWindowViewModel.cs b/WPF/VMagicMirrorConfig/ViewModel/MainWindowViewModel.cs index 5c64d6506..796a8f2be 100644 --- a/WPF/VMagicMirrorConfig/ViewModel/MainWindowViewModel.cs +++ b/WPF/VMagicMirrorConfig/ViewModel/MainWindowViewModel.cs @@ -64,7 +64,7 @@ public async void Initialize() await ModelResolver.Instance.Resolve().InitializeCustomMotionClipNamesAsync(); _runtimeHelper.Start(); ModelResolver.Instance.Resolve().Initialize(); - ModelResolver.Instance.Resolve().LoadSettingFromDefaultFile(); + ModelResolver.Instance.Resolve().InitializeAsync(); if (_settingModel.AutoLoadLastLoadedVrm.Value && !string.IsNullOrEmpty(_settingModel.LastVrmLoadFilePath)) {