在 Unity 中分离输入与其他脚本的最佳做法



我有一个输入脚本,它将触摸转换为方向(左、右、上、下(,在Update循环中以 (0-1( 的大小转换为方向,当检测到输入时,该脚本会触发UnityEvent

public class TouchAnalogStickInput : MonoBehaviour
{
[System.Serializable]
public class AnalogStickInputEvent : UnityEvent<Direction, float> { }
[Header("Events")]
[Space]
[Tooltip("Fired when a successful swipe occurs. Event Args: Swipe Direction, A normalized input magnitude between 0 and 1.")]
public AnalogStickInputEvent OnAnalogStickInput;
void Update()
{
...
if (successfulInputDetected)
{
OnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
}
}

我从 Unity 检查器订阅了这个OnAnalogStickInput事件,以调用我的CharacterController2D.Move方法,该方法采用方向和量级:

public class CharacterController2D : MonoBehaviour
{
public void Move(Direction movementDirection, float normalizedInputMagnitude)
{
// Move the character.
} 
}

然后我还有另一个GameObject,我希望使用相同的输入脚本进行旋转。 所以我TouchAnalogStickInputSnapRotator附加到这个GameObject,订阅OnAnalogStickInput事件来调用SnapRotator.Rotate

public class SnapRotator : MonoBehaviour
{
public void Rotate(Direction movementDirection, float normalizedInputMagnitude)
{
// Rotate object.
}
}

在这一点上,我已经意识到我不再负责从哪些游戏循环调用这些方法,例如,我应该Update中检测输入,在FixedUpdate中使用此输入进行移动,也许在我的情况下,我想在LateUpdate中最后进行旋转。相反CharacterController2D.MoveSnapRotator.Rotate都是从运行输入代码的Update循环中触发的。

我能想到的唯一其他选择可能是将输入脚本的代码重构为方法调用。然后让CharacterController2DSnapRotatorUpdate循环中调用此方法,根据需要在FixedUpdateLateUpdate循环中执行移动/旋转,例如:

public class CharacterController2D : MonoBehaviour
{
public TouchAnalogStickInput Input;
private var mMovementInfo;
void Update()
{
// Contains a Direction and a Normalized Input Magnitude 
mMovementInfo = Input.DetectInput();
}
void FixedUpdate()
{
if (mMovementInfo == Moved)
// Move the character.
} 
}

我的问题是:在 Unity 中解耦此类脚本的最佳实践是什么?还是我把可重用性看得太远/过于害怕在游戏开发中耦合子类/组件?

溶液

如果它对其他人有帮助,这是我的最终解决方案,在半 sudocode 中,归功于 Ruzihm:

用于存储有关检测到的输入的信息的实用程序类:

public class InputInfo
{
public Direction Direction { get; set; } = Direction.None;
public float NormalizedMagnitude { get; set; } = 0f;
public TouchPhase? CurrentTouchPhase { get; set; } = null;
public InputInfo(Direction direction, float normalizedMagnitude, TouchPhase currentTouchPhase)
{
Direction = direction;
NormalizedMagnitude = normalizedMagnitude;
CurrentTouchPhase = currentTouchPhase;
}
public InputInfo()
{
}
}

输入类:

public class TouchAnalogStickInput : MonoBehaviour
{
[System.Serializable]
public class AnalogStickInputEvent : UnityEvent<InputInfo> { }
[Header("Events")]
[Space]
[Tooltip("Fired from the Update loop when virtual stick movement occurs. Event Args: Swipe Direction, A normalized input magnitude between 0 and 1.")]
public AnalogStickInputEvent OnUpdateOnAnalogStickInput;
[Tooltip("Fired from the FixedUpdate loop when virtual stick movement occurs. Event Args: Swipe Direction, A normalized input magnitude between 0 and 1.")]
public AnalogStickInputEvent OnFixedUpdateOnAnalogStickInput;
[Tooltip("Fired from the LateUpdate loop when virtual stick movement occurs. Event Args: Swipe Direction, A normalized input magnitude between 0 and 1.")]
public AnalogStickInputEvent OnLateUpdateOnAnalogStickInput;
private bool mInputFlag;
private InputInfo mInputInfo;
void Update()
{
// Important - No input until proven otherwise, reset all members.
mInputFlag = false;
mInputInfo = new InputInfo();
// Logic to detect input
...
if (inputDetected)
{
mInputInfo.Direction = direction;
mInputInfo.NormalizedMagnitude = magnitude;
mInputInfo.CurrentTouchPhase = touch.phase;
// Now that the Input Info has been fully populated set the input detection flag.
mInputFlag = true;
// Fire Input Event to listeners
OnUpdateOnAnalogStickInput.Invoke(mInputInfo);
}
void FixedUpdate()
{
if (mInputFlag)
{
OnFixedUpdateOnAnalogStickInput.Invoke(mInputInfo);
}
}
void LateUpdate()
{
OnLateUpdateOnAnalogStickInput.Invoke(mInputInfo);
}
}
}

订阅OnFixedUpdateOnAnalogStickInput事件的字符控制器。

public class CharacterController2D : MonoBehaviour
{
public void Move(InputInfo inputInfo)
{
// Use inputInfo to decide how to move.
}
}

旋转类大致相同,但订阅OnLateUpdateOnAnalogStickInput事件。

这里有一种选择,它主要需要更改TouchAnalogStickInput类。

Update中设置输入状态标志并触发任何相关事件。只有这一次,您才会触发一个"OnUpdate"事件(在您的特定情况下,该事件不会注册任何内容(:

void Update()
{
...
inputFlag_AnalogStickInput = false;
...
if (successfulInputDetected)
{
inputFlag_AnalogStickInput = true;
OnUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
}

TouchAnalogStickInput.FixedUpdate中,调用OnFixedUpdateOnAnalogStickInput,这将在您的Move注册

void FixedUpdate()
{
...
if (inputFlag_AnalogStickInput)
{
OnFixedUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
}

以此类推,使用 LateUpdate,它会触发您的Rotate注册的事件。

void LateUpdate()
{
...
if (inputFlag_AnalogStickInput)
{
OnLateUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
}

当然,这些OnFixedUpdateOn...事件会向每一种FixedUpdate开火,只要这面旗帜是真的。对于大多数情况(包括Move(,这可能是合适的,但在其他情况下可能不是可取的。因此,您可以添加仅在更新后第一次FixedUpdate触发的其他事件。例如:

void Update()
{
...
firstFixedUpdateAfterUpdate = true;
inputFlag_AnalogStickInput = false;
...
if (successfulInputDetected)
{
inputFlag_AnalogStickInput = true;
OnUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag);
}
}

void FixedUpdate()
{
...
if (inputFlag_AnalogStickInput)
{
OnFixedUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag); 
}
if (inputFlag_AnalogStickInput && firstFixedUpdateAfterUpdate)  
{
OnFirstFixedUpdateOnAnalogStickInput.Invoke(mInputDirection, normalizedInputMag); 
}
...
firstFixedUpdateAfterUpdate = false;
}

希望这是有道理的。

您可能希望考虑自定义脚本执行顺序,并让您的手势处理器先于其他所有操作运行。

希望有帮助。 =(

最新更新