A game about forced loneliness, made by TACStudios
at master 317 lines 13 kB view raw
1using System; 2using System.Collections.Generic; 3using System.Reflection; 4using Unity.Multiplayer.Center.Analytics; 5using Unity.Multiplayer.Center.Common; 6using Unity.Multiplayer.Center.Onboarding; 7using Unity.Multiplayer.Center.Questionnaire; 8using Unity.Multiplayer.Center.Window.UI; 9using UnityEditor; 10using UnityEngine; 11using UnityEngine.UIElements; 12 13namespace Unity.Multiplayer.Center.Window 14{ 15 [Serializable] 16 internal class QuickstartCategory 17 { 18 [SerializeField] 19 public OnboardingSectionCategory Category; 20 21 [SerializeReference] 22 public IOnboardingSection[] Sections; 23 } 24 25 /// <summary> 26 /// This is the main view for the Quickstart tab. 27 /// Note that in the code, the Quickstart tab is referred to as the Getting Started tab. 28 /// </summary> 29 [Serializable] 30 internal class GettingStartedTabView : ITabView 31 { 32 const string k_SectionUssClass = "onboarding-section-category-container"; 33 const string k_CategoryButtonUssClass = "onboarding-category-button"; 34 const string k_OnboardingCategoriesUssClass = "onboarding-categories"; 35 const string k_OnboardingContentUssClass = "onboarding-content"; 36 37 [field: SerializeField] 38 public string Name { get; private set; } 39 40 public bool IsEnabled => PackageManagement.IsAnyMultiplayerPackageInstalled(); 41 42 public string ToolTip => IsEnabled ? "" : "Please install some multiplayer packages to access quickstart content."; 43 44 public VisualElement RootVisualElement { get; set; } 45 46 [SerializeField] 47 int m_SelectedCategory; 48 49 Dictionary<OnboardingSectionCategory, int> m_CategoryIndices; 50 51 VisualElement[] m_CategoryContainers; 52 53 [SerializeField] 54 QuickstartCategory[] m_SectionCategories; 55 /// <summary> 56 /// To find out if new section appeared, we need to keep track of the last section types we found. 57 /// </summary> 58 [SerializeField] 59 AvailableSectionTypes m_LastFoundSectionTypes; 60 61 public IMultiplayerCenterAnalytics MultiplayerCenterAnalytics { get; set; } 62 63 public GettingStartedTabView(string name = "Quickstart") 64 { 65 Name = name; 66 } 67 68 public void Refresh() 69 { 70 Debug.Assert(MultiplayerCenterAnalytics != null, "MultiplayerCenterAnalytics != null"); 71 UserChoicesObject.instance.OnSolutionSelectionChanged -= NotifyChoicesChanged; 72 UserChoicesObject.instance.OnSolutionSelectionChanged += NotifyChoicesChanged; 73 74 var currentSectionTypes = SectionsFinder.FindSectionTypes(); 75 76 if (m_SectionCategories == null || m_SectionCategories.Length == 0 || m_LastFoundSectionTypes.HaveTypesChanged(currentSectionTypes)) 77 { 78 m_LastFoundSectionTypes = currentSectionTypes; 79 ConstructSectionInstances(); 80 CreateViews(); 81 } 82 else if(RootVisualElement == null || RootVisualElement.childCount == 0) 83 { 84 CreateViews(); 85 } 86 } 87 88 public void Clear() 89 { 90 RootVisualElement?.Clear(); 91 if (m_SectionCategories == null) 92 return; 93 foreach (var category in m_SectionCategories) 94 { 95 if(category == null) continue; 96 foreach (var section in category.Sections) 97 { 98 section?.Unload(); 99 } 100 } 101 102 Array.Clear(m_SectionCategories, 0, m_SectionCategories.Length); 103 } 104 105 public void SetVisible(bool visible) 106 { 107 RootVisualElement.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None; 108 } 109 110 void ConstructSectionInstances() 111 { 112 var enumValues = Enum.GetValues(typeof(OnboardingSectionCategory)); 113 var allCategories = new QuickstartCategory[enumValues.Length]; 114 foreach (var categoryObject in enumValues) 115 { 116 var category = (OnboardingSectionCategory) categoryObject; 117 var categoryData = new QuickstartCategory {Category = category, Sections = Array.Empty<IOnboardingSection>()}; 118 allCategories[(int) category] = categoryData; 119 if (!m_LastFoundSectionTypes.TryGetValue(category, out var sectionTypes)) 120 { 121 continue; // no section for that category 122 } 123 124 categoryData.Sections = new IOnboardingSection[sectionTypes.Length]; 125 for (var index = 0; index < sectionTypes.Length; index++) 126 { 127 var sectionType = sectionTypes[index]; 128 var newSection = SectionFromType(sectionType); 129 130 // TODO: check what to do with null sections 131 if (newSection == null) continue; 132 133 categoryData.Sections[index] = newSection; 134 } 135 } 136 137 m_SectionCategories = allCategories; 138 } 139 140 void SetSelectedCategory(int categoryIndex) 141 { 142 m_SelectedCategory = categoryIndex; 143 for (var index = 0; index < m_CategoryContainers.Length; index++) 144 { 145 var categoryContainer = m_CategoryContainers[index]; 146 if(categoryContainer != null) 147 categoryContainer.style.display = index == categoryIndex ? DisplayStyle.Flex : DisplayStyle.None; 148 } 149 } 150 151 void CreateViews() 152 { 153 RootVisualElement ??= new VisualElement(); 154 RootVisualElement.Clear(); 155 156 if (QuickstartIsMissingView.ShouldShow) 157 { 158 RootVisualElement.Add(new QuickstartIsMissingView().RootVisualElement); 159 } 160 161 m_CategoryIndices = new Dictionary<OnboardingSectionCategory, int>(); 162 m_CategoryContainers = new VisualElement[m_SectionCategories.Length]; 163 164 var horizontalContainer = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal); 165 RootVisualElement.Add(horizontalContainer); 166 horizontalContainer.AddToClassList(StyleClasses.MainSplitView); 167 var buttonGroup = new ToggleButtonGroup() { allowEmptySelection = false, isMultipleSelection = false}; 168 buttonGroup.AddToClassList(k_OnboardingCategoriesUssClass); 169 buttonGroup.AddToClassList(StyleClasses.MainSplitViewLeft); 170 horizontalContainer.Add(buttonGroup); 171 172 var scrollView = new ScrollView(ScrollViewMode.Vertical) {horizontalScrollerVisibility = ScrollerVisibility.Hidden}; 173 scrollView.AddToClassList(StyleClasses.MainSplitViewRight); 174 scrollView.AddToClassList(k_OnboardingContentUssClass); 175 176 horizontalContainer.Add(scrollView); 177 178 var index = -1; 179 foreach (var categoryData in m_SectionCategories) 180 { 181 if (categoryData == null || categoryData.Sections.Length == 0) continue; 182 183 ++index; 184 var category = categoryData.Category; 185 var currentContainer = StartNewSection(scrollView, category); 186 scrollView.Add(currentContainer); 187 188 m_CategoryIndices[category] = index; 189 m_CategoryContainers[index] = currentContainer; 190 191 var button = new Button { text = SectionCategoryToString(category)}; 192 button.AddToClassList(k_CategoryButtonUssClass); 193 buttonGroup.Add(button); 194 195 CreateSectionViewsIn(currentContainer, categoryData); 196 } 197 198 // Hide the SplitView if we have nothing to show 199 var noContentToShow = index == -1; 200 horizontalContainer.style.display = noContentToShow ? DisplayStyle.None : DisplayStyle.Flex; 201 202 if (noContentToShow && !QuickstartIsMissingView.ShouldShow) 203 { 204 var noContentLabel = new Label("No content is available for the current selection in Netcode Solution and Hosting Model."); 205 noContentLabel.style.marginLeft = noContentLabel.style.marginRight = noContentLabel.style.marginTop = noContentLabel.style.marginBottom = 8; 206 RootVisualElement.Add(noContentLabel); 207 } 208 209 SetSelectedCategory(m_SelectedCategory); 210 ulong mask = (ulong) 1 << m_SelectedCategory; 211 buttonGroup.SetValueWithoutNotify(new ToggleButtonGroupState(mask, m_CategoryIndices.Count)); 212 213 // MTT-8918 Block the callback on register as it will always return index 0, 214 // which can result in a mismatch between toggle group and selected category. 215 var onCreateFrame = EditorApplication.timeSinceStartup; 216 buttonGroup.RegisterValueChangedCallback(evt => 217 { 218 if (Math.Abs(onCreateFrame - EditorApplication.timeSinceStartup) < 0.05f) 219 return; 220 221 var selectedIndex = evt.newValue.GetActiveOptions(stackalloc int[evt.newValue.length])[0]; 222 SetSelectedCategory(selectedIndex); 223 }); 224 NotifyChoicesChanged(); 225 } 226 227 void CreateSectionViewsIn(VisualElement currentContainer, QuickstartCategory categoryData) 228 { 229 foreach (var section in categoryData.Sections) 230 { 231 try 232 { 233 if (section is ISectionWithAnalytics sectionWithAnalytics) 234 { 235 var attribute = section.GetType().GetCustomAttribute<OnboardingSectionAttribute>(); 236 sectionWithAnalytics.AnalyticsProvider = new OnboardingSectionAnalyticsProvider(MultiplayerCenterAnalytics, 237 targetPackageId: attribute.TargetPackageId, sectionId: attribute.Id); 238 } 239 240 section.Load(); 241 section.Root.name = section.GetType().Name; 242 currentContainer.Add(section.Root); 243 } 244 catch (Exception e) 245 { 246 Debug.LogWarning($"Could not load onboarding section {section?.GetType()}: {e}"); 247 } 248 } 249 } 250 251 void NotifyChoicesChanged() 252 { 253 if (m_SectionCategories == null) 254 return; 255 256 foreach (var category in m_SectionCategories) 257 { 258 if (category == null) continue; 259 foreach (var section in category.Sections) 260 { 261 if (section is not ISectionDependingOnUserChoices dependentSection) continue; 262 263 try 264 { 265 dependentSection.HandleAnswerData(UserChoicesObject.instance.UserAnswers); 266 dependentSection.HandlePreset(UserChoicesObject.instance.Preset); 267 dependentSection.HandleUserSelectionData(UserChoicesObject.instance.SelectedSolutions); 268 } 269 catch (Exception e) 270 { 271 Debug.LogWarning($"Could not set data for onboarding section {section.GetType()}: {e}"); 272 } 273 } 274 } 275 } 276 277 static VisualElement StartNewSection(VisualElement parent, OnboardingSectionCategory category) 278 { 279 var container = new VisualElement(); 280 if (category != OnboardingSectionCategory.Intro) 281 { 282 var titleContainer = new VisualElement(); 283 titleContainer.AddToClassList(StyleClasses.ViewHeadline); 284 285 var title = new Label(SectionCategoryToString(category)); 286 titleContainer.Add(title); 287 container.Add(titleContainer); 288 } 289 290 container.AddToClassList(k_SectionUssClass); 291 parent.Add(container); 292 return container; 293 } 294 295 static IOnboardingSection SectionFromType(Type type) 296 { 297 var constructed = type.GetConstructor(Type.EmptyTypes)?.Invoke(null); 298 if (constructed is IOnboardingSection section) return section; 299 300 Debug.LogWarning($"Could not create onboarding section {type}"); 301 return null; 302 } 303 304 static string SectionCategoryToString(OnboardingSectionCategory category) 305 { 306 return category switch 307 { 308 OnboardingSectionCategory.Intro => "Intro", 309 OnboardingSectionCategory.Netcode => "Netcode and Tools", 310 OnboardingSectionCategory.ConnectingPlayers => "Connecting Players", 311 OnboardingSectionCategory.ServerInfrastructure => "Hosting", 312 OnboardingSectionCategory.Other => "Other", 313 _ => category.ToString() 314 }; 315 } 316 } 317}