Skip to content

Commit

Permalink
Merge pull request #1008 from malaybaku/feature/game_input_vrma_support
Browse files Browse the repository at this point in the history
Support VRM Animation in game input mode
  • Loading branch information
malaybaku authored Dec 30, 2023
2 parents a889fa7 + a4834f7 commit a00b154
Show file tree
Hide file tree
Showing 25 changed files with 1,100 additions and 366 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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");
Expand All @@ -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<string> _customMotionActionKeys = new();
private bool _customMotionActionKeysInitialized;

private bool _hasModel;
private Animator _animator;
private int _baseLayerIndex;
Expand All @@ -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;
Expand Down Expand Up @@ -73,13 +90,17 @@ public GameInputBodyMotionController(
IVRMLoadable vrmLoadable,
IMessageReceiver receiver,
BodyMotionModeController bodyMotionModeController,
VrmaRepository vrmaRepository,
VrmaMotionSetter vrmaMotionSetter,
GameInputBodyRootOrientationController rootOrientationController,
GameInputSourceSet sourceSet
)
{
_vrmLoadable = vrmLoadable;
_receiver = receiver;
_bodyMotionModeController = bodyMotionModeController;
_vrmaRepository = vrmaRepository;
_vrmaMotionSetter = vrmaMotionSetter;
_rootOrientationController = rootOrientationController;
_sourceSet = sourceSet;
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public enum GameInputStickAction

public enum GameInputButtonAction
{
None,
Custom = -1,
None = 0,
Jump,
Crouch,
Run,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,23 @@ public class GamepadGameInputSource : PresenterBase, IGameInputSource
IObservable<bool> IGameInputSource.GunFire => _gunFire;
IObservable<Unit> IGameInputSource.Jump => _jump;
IObservable<Unit> IGameInputSource.Punch => _punch;
IObservable<string> IGameInputSource.CustomMotion => _customMotion;

#endregion

private readonly XInputGamePad _gamepad;
private readonly IMessageReceiver _receiver;
private CompositeDisposable _disposable;

private readonly ReactiveProperty<Vector2> _moveInput = new ReactiveProperty<Vector2>();
private readonly ReactiveProperty<Vector2> _lookAroundInput = new ReactiveProperty<Vector2>();
private readonly ReactiveProperty<bool> _isCrouching = new ReactiveProperty<bool>();
private readonly ReactiveProperty<bool> _isRunning = new ReactiveProperty<bool>();
private readonly ReactiveProperty<bool> _gunFire = new ReactiveProperty<bool>();
private readonly Subject<Unit> _jump = new Subject<Unit>();
private readonly Subject<Unit> _punch = new Subject<Unit>();

private readonly ReactiveProperty<Vector2> _moveInput = new();
private readonly ReactiveProperty<Vector2> _lookAroundInput = new();
private readonly ReactiveProperty<bool> _isCrouching = new();
private readonly ReactiveProperty<bool> _isRunning = new();
private readonly ReactiveProperty<bool> _gunFire = new();
private readonly Subject<Unit> _jump = new();
private readonly Subject<Unit> _punch = new();
private readonly Subject<string> _customMotion = new();

private bool _isActive;
private GamepadGameInputKeyAssign _keyAssign = GamepadGameInputKeyAssign.LoadDefault();

Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@ public interface IGameInputSource

IObservable<Unit> Jump { get; }
IObservable<Unit> Punch { get; }

/// <summary> .vrma のカスタムモーションはココからkeyを指定して発火 </summary>
IObservable<string> CustomMotion { get; }
}
}
Loading

0 comments on commit a00b154

Please sign in to comment.