439 lines
12 KiB
C#
439 lines
12 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.XR;
|
|
|
|
namespace BlockSpace.Voxels
|
|
{
|
|
[DisallowMultipleComponent]
|
|
public sealed class AvatarCustomizerController : MonoBehaviour
|
|
{
|
|
private const float AvatarVoxelSizeScale = 0.5f;
|
|
|
|
private const float ButtonPressCooldown = 0.2f;
|
|
|
|
private const float ButtonShrinkDuration = 0.08f;
|
|
|
|
private const float ButtonSettleDuration = 0.12f;
|
|
|
|
private const float ButtonPressedScale = 0.84f;
|
|
|
|
private const float ButtonOvershootScale = 1.04f;
|
|
|
|
private const string ButtonThockResourcePath = "Thock";
|
|
|
|
private static readonly string[] SuccessClipAssetPaths = new string[3] { "Assets/success.wav", "Assets/success.mp3", "Assets/success.ogg" };
|
|
|
|
private const string UiRootPath = "UI";
|
|
|
|
private const string EditHeadPath = "UI/Edit Head";
|
|
|
|
private const string EditLeftPath = "UI/Edit Left";
|
|
|
|
private const string EditRightPath = "UI/Edit Right";
|
|
|
|
private const string StopEditingPath = "UI/Stop Editing";
|
|
|
|
private const string ApplyAndSavePath = "UI/Apply and Save";
|
|
|
|
private const string HeadGroupPath = "Origins/Head/Head Group";
|
|
|
|
private const string LeftGroupPath = "Origins/Left/Left Group";
|
|
|
|
private const string RightGroupPath = "Origins/Right/Right Group";
|
|
|
|
[SerializeField]
|
|
private VoxelGroupManager groupManager;
|
|
|
|
[SerializeField]
|
|
private VRVoxelBrush voxelBrush;
|
|
|
|
[SerializeField]
|
|
private Collider editHeadButton;
|
|
|
|
[SerializeField]
|
|
private Collider editLeftButton;
|
|
|
|
[SerializeField]
|
|
private Collider editRightButton;
|
|
|
|
[SerializeField]
|
|
private Collider stopEditingButton;
|
|
|
|
[SerializeField]
|
|
private Collider applyAndSaveButton;
|
|
|
|
[SerializeField]
|
|
private Transform headGroupTransform;
|
|
|
|
[SerializeField]
|
|
private Transform leftGroupTransform;
|
|
|
|
[SerializeField]
|
|
private Transform rightGroupTransform;
|
|
|
|
[SerializeField]
|
|
private AudioClip buttonThockClip;
|
|
|
|
[SerializeField]
|
|
private AudioClip applySuccessClip;
|
|
|
|
private VoxelGroup headEditorGroup;
|
|
|
|
private VoxelGroup leftEditorGroup;
|
|
|
|
private VoxelGroup rightEditorGroup;
|
|
|
|
private VoxelGroup activeEditorGroup;
|
|
|
|
private readonly Dictionary<Transform, Vector3> buttonBaseScales = new Dictionary<Transform, Vector3>();
|
|
|
|
private readonly Dictionary<Transform, Coroutine> buttonAnimationCoroutines = new Dictionary<Transform, Coroutine>();
|
|
|
|
private bool editorGroupsInitialized;
|
|
|
|
private bool loadedPersistedAvatar;
|
|
|
|
private bool editHeadInsidePrevious;
|
|
|
|
private bool editLeftInsidePrevious;
|
|
|
|
private bool editRightInsidePrevious;
|
|
|
|
private bool stopEditingInsidePrevious;
|
|
|
|
private bool applyAndSaveInsidePrevious;
|
|
|
|
private float nextButtonPressTime;
|
|
|
|
public void Configure(VoxelGroupManager manager, VRVoxelBrush brush)
|
|
{
|
|
if (manager != null)
|
|
{
|
|
groupManager = manager;
|
|
}
|
|
if (brush != null)
|
|
{
|
|
voxelBrush = brush;
|
|
}
|
|
ResolveSceneReferences();
|
|
EnsureButtonAudioAssigned();
|
|
EnsureApplySuccessAudioAssigned();
|
|
TryInitializeEditorGroups();
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
ResolveSceneReferences();
|
|
EnsureButtonAudioAssigned();
|
|
EnsureApplySuccessAudioAssigned();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
StopAllCoroutines();
|
|
StopEditing();
|
|
ResetAllButtonScales();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
ResolveSceneReferences();
|
|
if (TryInitializeEditorGroups())
|
|
{
|
|
HandleButton(editHeadButton, ref editHeadInsidePrevious, delegate
|
|
{
|
|
StartEditing(headEditorGroup);
|
|
});
|
|
HandleButton(editLeftButton, ref editLeftInsidePrevious, delegate
|
|
{
|
|
StartEditing(leftEditorGroup);
|
|
});
|
|
HandleButton(editRightButton, ref editRightInsidePrevious, delegate
|
|
{
|
|
StartEditing(rightEditorGroup);
|
|
});
|
|
HandleButton(stopEditingButton, ref stopEditingInsidePrevious, StopEditing);
|
|
HandleButton(applyAndSaveButton, ref applyAndSaveInsidePrevious, ApplyAndSave);
|
|
}
|
|
}
|
|
|
|
private void ResolveSceneReferences()
|
|
{
|
|
if (groupManager == null)
|
|
{
|
|
groupManager = ((VoxelGroupManager.Instance != null) ? VoxelGroupManager.Instance : UnityEngine.Object.FindObjectOfType<VoxelGroupManager>());
|
|
}
|
|
if (voxelBrush == null)
|
|
{
|
|
voxelBrush = ((VRVoxelBrush.Instance != null) ? VRVoxelBrush.Instance : UnityEngine.Object.FindObjectOfType<VRVoxelBrush>());
|
|
}
|
|
if (editHeadButton == null)
|
|
{
|
|
editHeadButton = FindCollider("UI/Edit Head");
|
|
}
|
|
if (editLeftButton == null)
|
|
{
|
|
editLeftButton = FindCollider("UI/Edit Left");
|
|
}
|
|
if (editRightButton == null)
|
|
{
|
|
editRightButton = FindCollider("UI/Edit Right");
|
|
}
|
|
if (stopEditingButton == null)
|
|
{
|
|
stopEditingButton = FindCollider("UI/Stop Editing");
|
|
}
|
|
if (applyAndSaveButton == null)
|
|
{
|
|
applyAndSaveButton = FindCollider("UI/Apply and Save");
|
|
}
|
|
if (headGroupTransform == null)
|
|
{
|
|
headGroupTransform = base.transform.Find("Origins/Head/Head Group");
|
|
}
|
|
if (leftGroupTransform == null)
|
|
{
|
|
leftGroupTransform = base.transform.Find("Origins/Left/Left Group");
|
|
}
|
|
if (rightGroupTransform == null)
|
|
{
|
|
rightGroupTransform = base.transform.Find("Origins/Right/Right Group");
|
|
}
|
|
}
|
|
|
|
private bool TryInitializeEditorGroups()
|
|
{
|
|
if (editorGroupsInitialized)
|
|
{
|
|
return true;
|
|
}
|
|
if (groupManager == null || headGroupTransform == null || leftGroupTransform == null || rightGroupTransform == null)
|
|
{
|
|
return false;
|
|
}
|
|
headEditorGroup = PrepareEditorGroup(headGroupTransform.gameObject, headEditorGroup, 61000);
|
|
leftEditorGroup = PrepareEditorGroup(leftGroupTransform.gameObject, leftEditorGroup, 61001);
|
|
rightEditorGroup = PrepareEditorGroup(rightGroupTransform.gameObject, rightEditorGroup, 61002);
|
|
if (headEditorGroup == null || leftEditorGroup == null || rightEditorGroup == null)
|
|
{
|
|
return false;
|
|
}
|
|
editorGroupsInitialized = true;
|
|
if (!loadedPersistedAvatar)
|
|
{
|
|
AvatarVoxelStorage.ApplyPayload(AvatarVoxelStorage.LoadLocalOrDefault(), headEditorGroup, leftEditorGroup, rightEditorGroup, groupManager, useLocalGridTransform: true);
|
|
loadedPersistedAvatar = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private VoxelGroup PrepareEditorGroup(GameObject groupObject, VoxelGroup existingGroup, ushort groupId)
|
|
{
|
|
if (groupObject == null || groupManager == null)
|
|
{
|
|
return null;
|
|
}
|
|
VoxelGroup voxelGroup = ((existingGroup != null) ? existingGroup : groupObject.GetComponent<VoxelGroup>());
|
|
if (voxelGroup == null)
|
|
{
|
|
voxelGroup = groupObject.AddComponent<VoxelGroup>();
|
|
}
|
|
AvatarVoxelStorage.ApplyGroupData(voxelGroup, AvatarVoxelStorage.CaptureGroup(voxelGroup), groupManager, useLocalGridTransform: true);
|
|
voxelGroup.SetVoxelSizeScale(0.5f);
|
|
voxelGroup.id = groupId;
|
|
return voxelGroup;
|
|
}
|
|
|
|
private void StartEditing(VoxelGroup group)
|
|
{
|
|
if (!(group == null))
|
|
{
|
|
activeEditorGroup = group;
|
|
VRHaptics.Pulse(XRNode.RightHand, 0.18f, 0.035f);
|
|
VRGroupSelector.Instance?.ClearSelectionForToolUse();
|
|
VRGroupSelector.Instance?.SetInteractionSuppressed(suppressed: true);
|
|
voxelBrush?.SetAvatarEditorTarget(group);
|
|
}
|
|
}
|
|
|
|
private void StopEditing()
|
|
{
|
|
activeEditorGroup = null;
|
|
voxelBrush?.ClearAvatarEditorTarget();
|
|
VRGroupSelector.Instance?.SetInteractionSuppressed(suppressed: false);
|
|
}
|
|
|
|
private void ApplyAndSave()
|
|
{
|
|
if (TryInitializeEditorGroups())
|
|
{
|
|
voxelBrush?.FinalizeAvatarEditorSessions();
|
|
AvatarVoxelPayload payload = AvatarVoxelStorage.CapturePayload(headEditorGroup, leftEditorGroup, rightEditorGroup);
|
|
AvatarVoxelStorage.SaveLocal(payload);
|
|
PhotonVoxelAvatar localOwnedInstance = PhotonVoxelAvatar.LocalOwnedInstance;
|
|
if (localOwnedInstance != null)
|
|
{
|
|
localOwnedInstance.ApplyAndBroadcastAvatar(payload);
|
|
}
|
|
VRHaptics.Pulse(XRNode.RightHand, 0.32f, 0.055f);
|
|
PlayApplySuccessFeedback();
|
|
}
|
|
}
|
|
|
|
public bool TryGetEditorGroups(out VoxelGroup headBody, out VoxelGroup left, out VoxelGroup right)
|
|
{
|
|
headBody = null;
|
|
left = null;
|
|
right = null;
|
|
if (!TryInitializeEditorGroups())
|
|
{
|
|
return false;
|
|
}
|
|
headBody = headEditorGroup;
|
|
left = leftEditorGroup;
|
|
right = rightEditorGroup;
|
|
if (headBody != null && left != null)
|
|
{
|
|
return right != null;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void HandleButton(Collider button, ref bool insidePrevious, Action action)
|
|
{
|
|
bool flag = IsHandInside(button, (voxelBrush != null) ? voxelBrush.LeftHandAnchor : null) || IsHandInside(button, (voxelBrush != null) ? voxelBrush.RightHandAnchor : null);
|
|
if (flag && !insidePrevious)
|
|
{
|
|
PlayButtonEnterAnimation(button);
|
|
if (Time.unscaledTime >= nextButtonPressTime)
|
|
{
|
|
nextButtonPressTime = Time.unscaledTime + 0.2f;
|
|
PlayButtonClickFeedback(button);
|
|
action?.Invoke();
|
|
}
|
|
}
|
|
insidePrevious = flag;
|
|
}
|
|
|
|
private void PlayButtonEnterAnimation(Collider button)
|
|
{
|
|
if (!(button == null))
|
|
{
|
|
Transform transform = button.transform;
|
|
if (!buttonBaseScales.TryGetValue(transform, out var value))
|
|
{
|
|
value = transform.localScale;
|
|
buttonBaseScales.Add(transform, value);
|
|
}
|
|
if (buttonAnimationCoroutines.TryGetValue(transform, out var value2) && value2 != null)
|
|
{
|
|
StopCoroutine(value2);
|
|
}
|
|
transform.localScale = value;
|
|
buttonAnimationCoroutines[transform] = StartCoroutine(AnimateButtonScale(transform, value));
|
|
}
|
|
}
|
|
|
|
private void PlayButtonClickFeedback(Collider button)
|
|
{
|
|
if (!(buttonThockClip == null) && !(button == null))
|
|
{
|
|
AudioSource.PlayClipAtPoint(buttonThockClip, button.bounds.center, 1f);
|
|
}
|
|
}
|
|
|
|
private IEnumerator AnimateButtonScale(Transform buttonTransform, Vector3 baseScale)
|
|
{
|
|
Vector3 pressedScale = baseScale * 0.84f;
|
|
Vector3 overshootScale = baseScale * 1.04f;
|
|
float elapsed = 0f;
|
|
while (elapsed < 0.08f)
|
|
{
|
|
if (buttonTransform == null)
|
|
{
|
|
yield break;
|
|
}
|
|
elapsed += Time.unscaledDeltaTime;
|
|
float t = Mathf.Clamp01(elapsed / 0.08f);
|
|
buttonTransform.localScale = Vector3.LerpUnclamped(baseScale, pressedScale, t);
|
|
yield return null;
|
|
}
|
|
elapsed = 0f;
|
|
while (elapsed < 0.12f)
|
|
{
|
|
if (buttonTransform == null)
|
|
{
|
|
yield break;
|
|
}
|
|
elapsed += Time.unscaledDeltaTime;
|
|
float num = Mathf.Clamp01(elapsed / 0.12f);
|
|
float num2 = num * num * (3f - 2f * num);
|
|
Vector3 localScale = ((num2 < 0.5f) ? Vector3.LerpUnclamped(pressedScale, overshootScale, num2 / 0.5f) : Vector3.LerpUnclamped(overshootScale, baseScale, (num2 - 0.5f) / 0.5f));
|
|
buttonTransform.localScale = localScale;
|
|
yield return null;
|
|
}
|
|
if (buttonTransform != null)
|
|
{
|
|
buttonTransform.localScale = baseScale;
|
|
buttonAnimationCoroutines[buttonTransform] = null;
|
|
}
|
|
}
|
|
|
|
private void ResetAllButtonScales()
|
|
{
|
|
foreach (KeyValuePair<Transform, Vector3> buttonBaseScale in buttonBaseScales)
|
|
{
|
|
if (buttonBaseScale.Key != null)
|
|
{
|
|
buttonBaseScale.Key.localScale = buttonBaseScale.Value;
|
|
}
|
|
}
|
|
buttonAnimationCoroutines.Clear();
|
|
}
|
|
|
|
private void EnsureButtonAudioAssigned()
|
|
{
|
|
if (buttonThockClip == null)
|
|
{
|
|
buttonThockClip = Resources.Load<AudioClip>("Thock");
|
|
}
|
|
}
|
|
|
|
private void EnsureApplySuccessAudioAssigned()
|
|
{
|
|
_ = applySuccessClip != null;
|
|
}
|
|
|
|
private void PlayApplySuccessFeedback()
|
|
{
|
|
if (!(applySuccessClip == null))
|
|
{
|
|
Vector3 position = ((applyAndSaveButton != null) ? applyAndSaveButton.bounds.center : base.transform.position);
|
|
AudioSource.PlayClipAtPoint(applySuccessClip, position, 1f);
|
|
}
|
|
}
|
|
|
|
private Collider FindCollider(string relativePath)
|
|
{
|
|
Transform transform = base.transform.Find(relativePath);
|
|
if (!(transform != null))
|
|
{
|
|
return null;
|
|
}
|
|
return transform.GetComponent<Collider>();
|
|
}
|
|
|
|
private static bool IsHandInside(Collider button, Transform handAnchor)
|
|
{
|
|
if (button == null || handAnchor == null)
|
|
{
|
|
return false;
|
|
}
|
|
Vector3 position = handAnchor.position;
|
|
return (button.ClosestPoint(position) - position).sqrMagnitude <= 1E-06f;
|
|
}
|
|
}
|
|
}
|