- PortalTestDestination practice range: Valorant-style hitscan rifle (recoil, spray/bloom, reload, controller support), debug HUD, practice-dummy targets with head/body hitboxes, hit/death/respawn animations, procedural SFX/FX - AK47 model (Quaternius, CC0) held via Animator IK, with fire/reload/equip anims - Intro cinematic (camera shots, title card, target beam-in, GO hand-off) - Scene dressing editor pass: CC0 textures, props, warehouse roof, lighting and post-processing; custom stylized skybox shader - CharacterMovement: camera recoil, shoulder offset, cinematic control lock - Unity CLI pipeline package; plus in-progress chat/Rive/player changes Asset credits: Assets/Models/Props/PROPS_CREDITS.txt (3 CC-BY 3.0 props), Assets/Textures/Range/TEXTURES_LICENSE.txt, Assets/Resources/*/..._LICENSE.txt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.0 KiB
C#
150 lines
5.0 KiB
C#
using ProceduralEditor;
|
|
using Rive;
|
|
using Rive.Components;
|
|
using System.Reflection.Metadata;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
|
|
/// Sends keyboard and gamepad navigation commands to the Rive menu and
|
|
/// activates the Play option when Enter/gamepad confirm is pressed
|
|
public class RiveNavigationController : MonoBehaviour
|
|
{
|
|
[Header("Rive")]
|
|
[Tooltip("RiveWidget that hosts the menu artboard.")]
|
|
[SerializeField] private RiveWidget riveWidget;
|
|
|
|
[Header("Scene Transition")]
|
|
[Tooltip("Object containing the SceneTransitionController component.")]
|
|
[SerializeField] private SceneTransitionController sceneTransitionController;
|
|
|
|
[Header("Menu Selection")]
|
|
[Tooltip("Total number of selectable Rive menu buttons.")]
|
|
[Min(1)]
|
|
[SerializeField] private int menuItemCount = 1;
|
|
|
|
|
|
private const string NextTriggerName = "nextButtonClicked";
|
|
private const string PreviousTriggerName = "previousButtonClicked";
|
|
private const int PlayIndex = 0;
|
|
|
|
private ViewModelInstanceTriggerProperty nextTrigger;
|
|
private ViewModelInstanceTriggerProperty previousTrigger;
|
|
|
|
private int selectedIndex;
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (riveWidget == null)
|
|
{
|
|
Debug.LogError($"{nameof(RiveNavigationController)}: riveWidget is not assigned.");
|
|
return;
|
|
}
|
|
|
|
if (sceneTransitionController == null)
|
|
{
|
|
Debug.LogError($"{nameof(RiveNavigationController)}: SceneTransitionController is not assigned.");
|
|
}
|
|
|
|
riveWidget.OnWidgetStatusChanged += HandleWidgetStatusChanged;
|
|
// In case the widget already finished loading before we subscribed.
|
|
HandleWidgetStatusChanged();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (riveWidget != null)
|
|
riveWidget.OnWidgetStatusChanged -= HandleWidgetStatusChanged;
|
|
}
|
|
|
|
private void HandleWidgetStatusChanged()
|
|
{
|
|
if (riveWidget.Status != WidgetStatus.Loaded)
|
|
return;
|
|
|
|
ViewModelInstance viewModelInstance = riveWidget.StateMachine?.ViewModelInstance;
|
|
if (viewModelInstance == null)
|
|
{
|
|
Debug.LogError($"{nameof(RiveNavigationController)}: ViewModelInstance is null. " +
|
|
"Set Data Binding Mode to Auto Bind Default on the Rive Widget.");
|
|
return;
|
|
}
|
|
|
|
// Cache both triggers once the artboard is ready so Update() doesn't do
|
|
// string lookups every frame.
|
|
nextTrigger = viewModelInstance.GetTriggerProperty(NextTriggerName);
|
|
previousTrigger = viewModelInstance.GetTriggerProperty(PreviousTriggerName);
|
|
|
|
if (nextTrigger == null)
|
|
Debug.LogError($"{nameof(RiveNavigationController)}: Trigger '{NextTriggerName}' not found on view model.");
|
|
if (previousTrigger == null)
|
|
Debug.LogError($"{nameof(RiveNavigationController)}: Trigger '{PreviousTriggerName}' not found on view model.");
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
Keyboard keyboard = Keyboard.current;
|
|
Gamepad gamepad = Gamepad.current;
|
|
|
|
bool movedRight = false;
|
|
bool movedLeft = false;
|
|
bool confirmPressed = false;
|
|
|
|
if (keyboard != null)
|
|
{
|
|
movedRight |= keyboard.rightArrowKey.wasPressedThisFrame;
|
|
movedLeft |= keyboard.leftArrowKey.wasPressedThisFrame;
|
|
confirmPressed |= keyboard.enterKey.wasPressedThisFrame || keyboard.numpadEnterKey.wasPressedThisFrame;
|
|
}
|
|
|
|
if (gamepad != null)
|
|
{
|
|
// D-pad and left stick both move the selection — leftStick.left/right are the
|
|
// Input System's built-in synthetic buttons derived from the stick's deflection,
|
|
// so this fires once per push just like a button, not continuously while held.
|
|
movedRight |= gamepad.dpad.right.wasPressedThisFrame || gamepad.leftStick.right.wasPressedThisFrame;
|
|
movedLeft |= gamepad.dpad.left.wasPressedThisFrame || gamepad.leftStick.left.wasPressedThisFrame;
|
|
confirmPressed |= gamepad.buttonSouth.wasPressedThisFrame;
|
|
}
|
|
|
|
if (movedRight)
|
|
{
|
|
MoveSelection(+1);
|
|
nextTrigger?.Trigger();
|
|
}
|
|
|
|
if (movedLeft)
|
|
{
|
|
MoveSelection(-1);
|
|
previousTrigger?.Trigger();
|
|
}
|
|
|
|
if (confirmPressed && selectedIndex == PlayIndex)
|
|
{
|
|
if (sceneTransitionController == null)
|
|
{
|
|
Debug.LogError($"{nameof(RiveNavigationController)}: SceneTransitionController is not assigned.");
|
|
|
|
return;
|
|
}
|
|
|
|
sceneTransitionController.LoadGameplayScene();
|
|
}
|
|
}
|
|
|
|
private void MoveSelection(int direction)
|
|
{
|
|
if (menuItemCount <= 0)
|
|
return;
|
|
|
|
selectedIndex += direction;
|
|
|
|
if (selectedIndex >= menuItemCount)
|
|
selectedIndex = 0;
|
|
|
|
if (selectedIndex < 0)
|
|
selectedIndex = menuItemCount - 1;
|
|
|
|
Debug.Log($"Rive menu selection index: {selectedIndex}");
|
|
}
|
|
}
|