Added indicators to round 1 that show the video load status for each player.

This commit is contained in:
2025-09-15 15:16:15 -04:00
parent 2b94964027
commit 7f93ebe6b2
27 changed files with 3024 additions and 1483 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 16d6ecfcfa76a4e4abc298cf18be9ae7
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,460 @@
using Newtonsoft.Json.Linq;
using UdonSharp;
using UnityEngine;
using VRC.SDK3.Components.Video;
using VRC.SDK3.Image;
using VRC.SDK3.UdonNetworkCalling;
using VRC.SDK3.Video.Components.Base;
using VRC.SDKBase;
using VRC.Udon.Common.Interfaces;
public enum ClueScreenType
{
Blank,
Video,
Map
}
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class CaseVideoSyncPlayer : UdonSharpBehaviour
{
[SerializeField] private CaseManager _CaseManager;
[SerializeField] private BaseVRCVideoPlayer _VideoPlayer;
/*[SerializeField]*/ private float _SyncFrequency = 5.0f;
[Header("Meshes")]
[SerializeField] private MeshRenderer _BlankScreenMesh;
[SerializeField] private MeshRenderer _VideoScreenMesh;
[SerializeField] private MeshRenderer _MapScreenMesh;
[Header("Materials")]
[SerializeField] private Material _MapScreenMaterial;
[SerializeField] private Texture2D _PlaceholderMapTexture;
[Header("Video Status")]
[SerializeField] private VideoLoadIndicator[] _VideoLoadIndicators;
[UdonSynced, FieldChangeCallback(nameof(SubMapIndex))] private int _SubMapIndex = 0;
[UdonSynced, FieldChangeCallback(nameof(ShowScreen))] private ClueScreenType _ShowScreen = ClueScreenType.Blank;
[UdonSynced, FieldChangeCallback(nameof(FlashCorrectAnswer))] private bool _FlashCorrectAnswer = false;
[UdonSynced, FieldChangeCallback(nameof(VideoIndex))] private int _VideoIndex = -1;
[UdonSynced, FieldChangeCallback(nameof(TimeAndOffset))] private Vector2 _TimeAndOffset;
[UdonSynced, FieldChangeCallback(nameof(PlayVideo))] private bool _VideoIsPlaying;
private VRCImageDownloader _MapDownloader;
private Texture2D[] _MapImages = new Texture2D[0];
private IUdonEventReceiver _UdonEventReceiverThis;
private int[] _CachedMapIndices = new int[0];
private int _MapDownloadIndex = 0;
private bool _MapDownloadsInProgress = false;
private bool _UseFallback = false;
private const int IMAGES_PER_MAP_ATLAS = 6;
void Start()
{
_MapDownloader = new VRCImageDownloader();
_UdonEventReceiverThis = (IUdonEventReceiver)this;
_MapScreenMesh.sharedMaterial = _MapScreenMaterial;
UpdateMap(false);
}
void OnDestroy()
{
_MapDownloader.Dispose();
}
[NetworkCallable]
public void QueueMapDownloads(int[] MapIndices)
{
if (_MapDownloadsInProgress)
{
// Concatenate both caches into a new bigger cache and await new downloads as usual
int[] NewMapIndicesCache = new int[_CachedMapIndices.Length + MapIndices.Length];
Texture2D[] NewMapImages = new Texture2D[_CachedMapIndices.Length + MapIndices.Length];
for (int i = 0; i < _CachedMapIndices.Length; i++)
{
NewMapIndicesCache[i] = _CachedMapIndices[i];
NewMapImages[i] = _MapImages[i];
}
for (int i = 0; i < MapIndices.Length; i++)
{
NewMapIndicesCache[i + _CachedMapIndices.Length] = MapIndices[i];
NewMapImages[i + _CachedMapIndices.Length] = _PlaceholderMapTexture;
}
_CachedMapIndices = NewMapIndicesCache;
_MapImages = NewMapImages;
}
else
{
_MapDownloadsInProgress = true;
_CachedMapIndices = MapIndices;
_MapImages = new Texture2D[_CachedMapIndices.Length];
for (int i = 0; i < _MapImages.Length; i++)
{
_MapImages[i] = _PlaceholderMapTexture;
}
_SubMapIndex = 0;
_MapDownloadIndex = 0;
LoadMapFromIndex(_CachedMapIndices[_MapDownloadIndex]);
}
}
private void LoadMapFromIndex(int MapIndex)
{
VRCUrl MapURL = _CaseManager.GetMap(MapIndex);
TextureInfo AdditionalTextureInfo = new TextureInfo();
AdditionalTextureInfo.WrapModeU = TextureWrapMode.Clamp;
AdditionalTextureInfo.WrapModeV = TextureWrapMode.Clamp;
AdditionalTextureInfo.GenerateMipMaps = true;
_MapDownloader.DownloadImage(MapURL, null, _UdonEventReceiverThis, AdditionalTextureInfo);
}
public override void OnImageLoadSuccess(IVRCImageDownload Result)
{
_MapImages[_MapDownloadIndex] = Result.Result;
int MapPage = SubMapIndex / IMAGES_PER_MAP_ATLAS;
if (MapPage == _MapDownloadIndex)
{
_MapScreenMaterial.SetTexture("_EmissionMap", _MapImages[MapPage]);
}
_MapDownloadIndex++;
if (_MapDownloadIndex >= _CachedMapIndices.Length)
{
_MapDownloadsInProgress = false;
}
else
{
LoadMapFromIndex(_CachedMapIndices[_MapDownloadIndex]);
}
base.OnImageLoadSuccess(Result);
}
public override void OnImageLoadError(IVRCImageDownload Result)
{
_MapDownloadsInProgress = false;
base.OnImageLoadError(Result);
}
[NetworkCallable]
public void NextCorrectAnswerFrame()
{
if (FlashCorrectAnswer)
{
SubMapIndex = (SubMapIndex == 4) ? 3 : 4;
SendCustomEventDelayedSeconds(nameof(NextCorrectAnswerFrame), 0.2f);
}
else
{
_VideoPlayer.Stop();
SubMapIndex = 0;
ShowScreen = ClueScreenType.Blank;
RequestSerialization();
}
}
private void UpdateMap(bool SyncResult = true)
{
int MapPage = SubMapIndex / IMAGES_PER_MAP_ATLAS;
int SubmapIndexWrapped = SubMapIndex % IMAGES_PER_MAP_ATLAS;
if (MapPage < _MapImages.Length)
{
switch (SubmapIndexWrapped)
{
case 0: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.0f, 0.66666666f)); break;
case 1: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.5f, 0.66666666f)); break;
case 2: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.0f, 0.33333333f)); break;
case 3: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.5f, 0.33333333f)); break;
case 4: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.0f, 0.0f)); break;
case 5: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.5f, 0.0f)); break;
}
_MapScreenMaterial.SetTexture("_EmissionMap", _MapImages[MapPage]);
ShowScreen = ClueScreenType.Map;
if (SyncResult)
{
RequestSerialization();
}
}
}
private void _LoadVideo_Private()
{
TryLoadURL();
}
public void TryLoadURL()
{
if (VideoIndex >= 0 && VideoIndex < _CaseManager.GetVideoCount())
{
_VideoPlayer.LoadURL(_CaseManager.GetVideo(VideoIndex, _UseFallback));
SetVideoLoadStatus(IndicationStatus.Loading);
}
else
{
Debug.LogWarning("[CaseVideoSyncPlayer] Index is out of range.");
}
}
private void _PlayVideo_Private()
{
_VideoPlayer.Play();
}
private void _StopVideo_Private()
{
_VideoPlayer.Stop();
_UseFallback = false;
VideoIndex = -1;
}
public override void OnVideoReady()
{
Debug.Log("[CaseVideoSyncPlayer] Video is ready.");
if (_VideoIsPlaying)
{
_PlayVideo_Private();
}
SetVideoLoadStatus(IndicationStatus.LoadSuccess);
base.OnVideoReady();
}
public override void OnVideoError(VideoError VideoError)
{
switch(VideoError)
{
case VideoError.Unknown:
Debug.LogError("[CaseVideoSyncPlayer] Unknown playback error.");
break;
case VideoError.InvalidURL:
Debug.LogError("[CaseVideoSyncPlayer] Invalid URL.");
break;
case VideoError.AccessDenied:
Debug.LogError("[CaseVideoSyncPlayer] Access denied.");
break;
case VideoError.PlayerError:
Debug.LogError("[CaseVideoSyncPlayer] Error with video player.");
break;
case VideoError.RateLimited:
Debug.LogError("[CaseVideoSyncPlayer] Rate limited.");
break;
}
if (_UseFallback)
{
_StopVideo_Private();
}
else
{
Debug.Log("[CaseVideoSyncPlayer] Attempting fallback in 5 seconds...");
SendCustomEventDelayedSeconds(nameof(TryLoadFallbackURL), 5.5f);
}
SetVideoLoadStatus(IndicationStatus.LoadFailure);
base.OnVideoError(VideoError);
}
public void TryLoadFallbackURL()
{
_UseFallback = true;
TryLoadURL();
}
public override void OnVideoStart()
{
if (_VideoIsPlaying)
{
ShowScreen = ClueScreenType.Video;
//UpdateTimeAndOffset();
}
else
{
_VideoPlayer.Stop();
}
base.OnVideoStart();
}
public override void OnVideoEnd()
{
PlayVideo = false;
ShowScreen = ClueScreenType.Blank;
RequestSerialization();
base.OnVideoEnd();
}
public void SetVideoLoadStatus(IndicationStatus Status)
{
foreach (VideoLoadIndicator Indicator in _VideoLoadIndicators)
{
if (Indicator.GetOwner() == Networking.LocalPlayer.displayName)
{
Indicator.IndicateStatus = Status;
}
}
}
private void UpdateTimeAndOffset()
{
if (Networking.IsOwner(gameObject))
{
TimeAndOffset = new Vector2(_VideoPlayer.GetTime(), (float)Networking.GetServerTimeInSeconds());
if (_SyncFrequency > 0.0f)
{
SendCustomEventDelayedSeconds(nameof(UpdateTimeAndOffset), _SyncFrequency);
}
}
else
{
Resync();
}
RequestSerialization();
}
public void Resync()
{
_VideoPlayer.SetTime(TimeAndOffset.x + ((float)Networking.GetServerTimeInSeconds() - TimeAndOffset.y));
}
[NetworkCallable]
public void ClearScreen()
{
FlashCorrectAnswer = false;
ShowScreen = ClueScreenType.Blank;
}
private void SwapToScreen(ClueScreenType Screen)
{
if (ShowScreen == Screen) return;
switch (Screen)
{
case ClueScreenType.Blank:
{
_BlankScreenMesh.transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f);
_VideoScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f);
_MapScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f);
PlayVideo = false;
break;
}
case ClueScreenType.Video:
{
_BlankScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f);
_VideoScreenMesh.transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f);
_MapScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f);
break;
}
case ClueScreenType.Map:
{
_BlankScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f);
_VideoScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f);
_MapScreenMesh.transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f);
PlayVideo = false;
break;
}
}
RequestSerialization();
}
public int SubMapIndex
{
set
{
_SubMapIndex = value;
UpdateMap(!FlashCorrectAnswer);
}
get => _SubMapIndex;
}
public ClueScreenType ShowScreen
{
set
{
SwapToScreen(value);
_ShowScreen = value;
}
get => _ShowScreen;
}
public bool FlashCorrectAnswer
{
set
{
_FlashCorrectAnswer = value;
SendCustomNetworkEvent(NetworkEventTarget.Self, nameof(NextCorrectAnswerFrame));
RequestSerialization();
}
get => _FlashCorrectAnswer;
}
public int VideoIndex
{
set
{
_VideoIndex = value;
if (_VideoIndex >= 0)
{
_UseFallback = false;
_LoadVideo_Private();
}
RequestSerialization();
}
get => _VideoIndex;
}
public bool PlayVideo
{
set
{
_VideoIsPlaying = value;
if (_VideoIsPlaying) _PlayVideo_Private();
else _StopVideo_Private();
RequestSerialization();
}
get => _VideoIsPlaying;
}
public Vector2 TimeAndOffset
{
set
{
_TimeAndOffset = value;
if (!Networking.IsOwner(gameObject))
{
Resync();
}
}
get => _TimeAndOffset;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6e7dbaee3b57f2e4fbdc7204cc644995
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,239 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c333ccfdd0cbdbc4ca30cef2dd6e6b9b, type: 3}
m_Name: VideoLoadIndicator
m_EditorClassIdentifier:
serializedUdonProgramAsset: {fileID: 11400000, guid: 055b39ce29e47bf43919be0d41337443,
type: 2}
udonAssembly:
assemblyError:
sourceCsScript: {fileID: 11500000, guid: b5910303773feb54d805c485ebec3ed4, type: 3}
scriptVersion: 2
compiledVersion: 2
behaviourSyncMode: 4
hasInteractEvent: 0
scriptID: 3627360377817521765
serializationData:
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes:
- Name: fieldDefinitions
Entry: 7
Data: 0|System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[UdonSharp.Compiler.FieldDefinition,
UdonSharp.Editor]], mscorlib
- Name: comparer
Entry: 7
Data: 1|System.Collections.Generic.GenericEqualityComparer`1[[System.String,
mscorlib]], mscorlib
- Name:
Entry: 8
Data:
- Name:
Entry: 12
Data: 3
- Name:
Entry: 7
Data:
- Name: $k
Entry: 1
Data: _Owner
- Name: $v
Entry: 7
Data: 2|UdonSharp.Compiler.FieldDefinition, UdonSharp.Editor
- Name: <Name>k__BackingField
Entry: 1
Data: _Owner
- Name: <UserType>k__BackingField
Entry: 7
Data: 3|System.RuntimeType, mscorlib
- Name:
Entry: 1
Data: System.String, mscorlib
- Name:
Entry: 8
Data:
- Name: <SystemType>k__BackingField
Entry: 9
Data: 3
- Name: <SyncMode>k__BackingField
Entry: 7
Data: System.Nullable`1[[UdonSharp.UdonSyncMode, UdonSharp.Runtime]], mscorlib
- Name:
Entry: 3
Data: 1
- Name:
Entry: 8
Data:
- Name: <IsSerialized>k__BackingField
Entry: 5
Data: false
- Name: _fieldAttributes
Entry: 7
Data: 4|System.Collections.Generic.List`1[[System.Attribute, mscorlib]], mscorlib
- Name:
Entry: 12
Data: 1
- Name:
Entry: 7
Data: 5|UdonSharp.UdonSyncedAttribute, UdonSharp.Runtime
- Name:
Entry: 8
Data:
- Name:
Entry: 13
Data:
- Name:
Entry: 8
Data:
- Name:
Entry: 8
Data:
- Name:
Entry: 8
Data:
- Name:
Entry: 7
Data:
- Name: $k
Entry: 1
Data: _IndicateStatus
- Name: $v
Entry: 7
Data: 6|UdonSharp.Compiler.FieldDefinition, UdonSharp.Editor
- Name: <Name>k__BackingField
Entry: 1
Data: _IndicateStatus
- Name: <UserType>k__BackingField
Entry: 7
Data: 7|System.RuntimeType, mscorlib
- Name:
Entry: 1
Data: IndicationStatus, Assembly-CSharp
- Name:
Entry: 8
Data:
- Name: <SystemType>k__BackingField
Entry: 7
Data: 8|System.RuntimeType, mscorlib
- Name:
Entry: 1
Data: System.Int32, mscorlib
- Name:
Entry: 8
Data:
- Name: <SyncMode>k__BackingField
Entry: 7
Data: System.Nullable`1[[UdonSharp.UdonSyncMode, UdonSharp.Runtime]], mscorlib
- Name:
Entry: 3
Data: 1
- Name:
Entry: 8
Data:
- Name: <IsSerialized>k__BackingField
Entry: 5
Data: false
- Name: _fieldAttributes
Entry: 7
Data: 9|System.Collections.Generic.List`1[[System.Attribute, mscorlib]], mscorlib
- Name:
Entry: 12
Data: 2
- Name:
Entry: 7
Data: 10|UdonSharp.UdonSyncedAttribute, UdonSharp.Runtime
- Name:
Entry: 8
Data:
- Name:
Entry: 7
Data: 11|UdonSharp.FieldChangeCallbackAttribute, UdonSharp.Runtime
- Name:
Entry: 8
Data:
- Name:
Entry: 13
Data:
- Name:
Entry: 8
Data:
- Name:
Entry: 8
Data:
- Name:
Entry: 8
Data:
- Name:
Entry: 7
Data:
- Name: $k
Entry: 1
Data: _IndicatorMesh
- Name: $v
Entry: 7
Data: 12|UdonSharp.Compiler.FieldDefinition, UdonSharp.Editor
- Name: <Name>k__BackingField
Entry: 1
Data: _IndicatorMesh
- Name: <UserType>k__BackingField
Entry: 7
Data: 13|System.RuntimeType, mscorlib
- Name:
Entry: 1
Data: UnityEngine.MeshRenderer, UnityEngine.CoreModule
- Name:
Entry: 8
Data:
- Name: <SystemType>k__BackingField
Entry: 9
Data: 13
- Name: <SyncMode>k__BackingField
Entry: 7
Data: System.Nullable`1[[UdonSharp.UdonSyncMode, UdonSharp.Runtime]], mscorlib
- Name:
Entry: 6
Data:
- Name:
Entry: 8
Data:
- Name: <IsSerialized>k__BackingField
Entry: 5
Data: false
- Name: _fieldAttributes
Entry: 7
Data: 14|System.Collections.Generic.List`1[[System.Attribute, mscorlib]], mscorlib
- Name:
Entry: 12
Data: 0
- Name:
Entry: 13
Data:
- Name:
Entry: 8
Data:
- Name:
Entry: 8
Data:
- Name:
Entry: 8
Data:
- Name:
Entry: 13
Data:
- Name:
Entry: 8
Data:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 04cac9381d0d46643a6e6fefe6d3dad8
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,62 @@
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
public enum IndicationStatus
{
Idle,
Loading,
LoadSuccess,
LoadFailure
}
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class VideoLoadIndicator : UdonSharpBehaviour
{
[UdonSynced] private string _Owner = null;
[UdonSynced, FieldChangeCallback(nameof(IndicateStatus))] private IndicationStatus _IndicateStatus = IndicationStatus.Idle;
private MeshRenderer _IndicatorMesh;
void Start()
{
_IndicatorMesh = GetComponent<MeshRenderer>();
_Owner = Networking.LocalPlayer.displayName;
}
public override void OnOwnershipTransferred(VRCPlayerApi Player)
{
_Owner = Player.displayName;
RequestSerialization();
base.OnOwnershipTransferred(Player);
}
public string GetOwner()
{
return _Owner;
}
public IndicationStatus IndicateStatus
{
set
{
_IndicateStatus = value;
switch(_IndicateStatus)
{
case IndicationStatus.Idle: _IndicatorMesh.material.SetColor("_Color", Color.black); break;
case IndicationStatus.Loading: _IndicatorMesh.material.SetColor("_Color", Color.yellow); break;
case IndicationStatus.LoadSuccess: _IndicatorMesh.material.SetColor("_Color", Color.green); break;
case IndicationStatus.LoadFailure: _IndicatorMesh.material.SetColor("_Color", Color.red); break;
}
Debug.Log("[VideoLoadIndicator] Setting load status to " + (int)_IndicateStatus);
RequestSerialization();
}
get => _IndicateStatus;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5910303773feb54d805c485ebec3ed4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: