test
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# Changelog
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2024-08-28
|
||||
|
||||
### Changed
|
||||
- Become a core package
|
||||
|
||||
### Fixed
|
||||
- Scoring of distributed authority hosting model in the Recommendations tab
|
||||
- Minor wording and style improvement
|
||||
- Packages that are not fundamentally incompatible with game specs are marked again as "Not Recommended" instead of "Incompatible"
|
||||
|
||||
## [1.0.0-pre.3] - 2024-08-21
|
||||
|
||||
### Fixed
|
||||
- The Quickstart tab was not automatically shown after installing packages
|
||||
- Made the UI look more responsive when reopening the window during package installation
|
||||
- Added vertical Scrollbar to the Game specifications section
|
||||
|
||||
### Changed
|
||||
- Always installing the latest Netcode for GameObjects 2.0.0-pre.x when it is selected as Netcode solution
|
||||
- Made loading wheel more visible when installing packages (dark mode)
|
||||
- Sort the game genre list alphabetically in the Recommendations tab
|
||||
|
||||
## [1.0.0-pre.2] - 2024-08-01
|
||||
|
||||
### Changed
|
||||
- Widgets are now recommended for Netcode for Entities.
|
||||
|
||||
## [1.0.0-pre.1] - 2024-07-29
|
||||
|
||||
### Fixed
|
||||
- Quickstart Tab: Show HelpBox if no quickstart package is installed but Quickstart Tab is visible.
|
||||
|
||||
## [0.4.0] - 2024-07-19
|
||||
|
||||
### Added
|
||||
- Added Recommendation for Distributed Authority hosting model with Netcode for GameObjects.
|
||||
- Undo support for Recommendation tab.
|
||||
- Added status Label at the bottom for package related information.
|
||||
|
||||
### Changed
|
||||
- Refined the Netcode Recommendation.
|
||||
- The Quickstart tab is deactivated if no multiplayer-related package is installed.
|
||||
|
||||
## [0.3.0] - 2024-06-06
|
||||
|
||||
### Added
|
||||
- Added analytics to the package
|
||||
|
||||
### Changed
|
||||
- **New user interface for the recommendations tab**
|
||||
- Multiplayer Center no longer upgrades packages that are embedded, linked locally, installed via Git or local Tarball
|
||||
- Window can now be found under `Window > Multiplayer > Multiplayer Center`
|
||||
- Updated the recommendation of the Widgets package (only compatible with Netcode for GameObjects at the moment)
|
||||
- Updated the tooltips and various texts in the recommendations tab
|
||||
|
||||
## [0.2.1] - 2024-04-25
|
||||
|
||||
Non user-facing changes only
|
||||
|
||||
## [0.2.0] - 2024-04-24
|
||||
|
||||
**Added**
|
||||
|
||||
- Automatic installation of the Getting Started content.
|
||||
- Changed API for the getting started content.
|
||||
|
||||
## [0.1.0] - 2024-04-22
|
||||
|
||||
This is the first release of *com.unity.multiplayer.center*. The package provides a new Window available at `Window > Multiplayer Center` that gives a starting point to create a multiplayer game.
|
||||
|
||||
The window consists of two tabs: `Recommendations` and `Getting Started`.
|
||||
- The `Recommendations` tab provides a customized list of packages and solutions to use based on the characteristics of your multiplayer game.
|
||||
- The `Getting Started` tab provides a list of resources based on the packages that you have installed.
|
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3bbd27b3dd7be4a74aeab8da6b0af5cc
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f247964bd405431fbd31840f97bef608
|
||||
timeCreated: 1700734124
|
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores what the user answered in the GameSpecs questionnaire. The Preset is not included here.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AnswerData
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of answers the user has given so far.
|
||||
/// </summary>
|
||||
public List<AnsweredQuestion> Answers = new();
|
||||
|
||||
/// <summary>
|
||||
/// Makes a deep copy of the object.
|
||||
/// </summary>
|
||||
/// <returns>The clone</returns>
|
||||
public AnswerData Clone()
|
||||
{
|
||||
return JsonUtility.FromJson(JsonUtility.ToJson(this), typeof(AnswerData)) as AnswerData;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Answer to a single game spec question.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AnsweredQuestion
|
||||
{
|
||||
/// <summary>
|
||||
/// The question identifier as defined in the game spec questionnaire.
|
||||
/// </summary>
|
||||
public string QuestionId;
|
||||
|
||||
/// <summary>
|
||||
/// The answers selected by the user (most often, it contains only one element).
|
||||
/// </summary>
|
||||
public List<string> Answers;
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89b6d933e4994d0980f6b48dc2b8779c
|
||||
timeCreated: 1695397313
|
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Unity.Multiplayer.Center.Common.Analytics;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Onboarding section metadata to be picked up by the multiplayer center.
|
||||
/// This can only be used once per type. If you wish to make the same section appear in multiple categories/conditions,
|
||||
/// please create two types inheriting from the same base class.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class OnboardingSectionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The UI category the section will fall into.
|
||||
/// </summary>
|
||||
public OnboardingSectionCategory Category { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The id of that section (defines uniqueness and whether priority should be used)
|
||||
/// </summary>
|
||||
public readonly string Id;
|
||||
|
||||
/// <summary>
|
||||
/// Optional: condition to display the section. By default, if the type exists in the project, the section is shown.
|
||||
/// </summary>
|
||||
public DisplayCondition DisplayCondition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: dependency on a certain hosting model choice.
|
||||
/// </summary>
|
||||
public SelectedSolutionsData.HostingModel HostingModelDependency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: dependency on a certain netcode choice.
|
||||
/// </summary>
|
||||
public SelectedSolutionsData.NetcodeSolution NetcodeDependency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: priority in case several onboarding sections are defined for the same package/id.
|
||||
/// Use-case: new version of a package needs a different onboarding and overrides what we ship with the Multiplayer Center.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: this is the order in which the sections will be displayed in the UI within the section.
|
||||
/// (the higher the Order value, the further down)
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: the package identifier that this section is related to, e.g. "com.unity.transport".
|
||||
/// </summary>
|
||||
public string TargetPackageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the attribute.
|
||||
/// </summary>
|
||||
/// <param name="category">The section category.</param>
|
||||
/// <param name="id">The identifier.</param>
|
||||
public OnboardingSectionAttribute(OnboardingSectionCategory category, string id)
|
||||
{
|
||||
Category = category;
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The UI category the section will fall into.
|
||||
/// </summary>
|
||||
public enum OnboardingSectionCategory
|
||||
{
|
||||
/// <summary>
|
||||
/// Comes at the top and should cover overarching topics for beginners
|
||||
/// </summary>
|
||||
Intro,
|
||||
|
||||
/// <summary>
|
||||
/// Section about the fundamentals of gameplay synchronization implementation and debugging.
|
||||
/// This includes netcode and tools related to netcode, as well as alternative solutions.
|
||||
/// </summary>
|
||||
Netcode,
|
||||
|
||||
/// <summary>
|
||||
/// Section gathering information about connecting players together, such as lobbies, voice chat, matchmaking
|
||||
/// and widgets.
|
||||
/// </summary>
|
||||
ConnectingPlayers,
|
||||
|
||||
/// <summary>
|
||||
/// Section gathering information about deploying, running and optimizing a game server.
|
||||
/// </summary>
|
||||
ServerInfrastructure,
|
||||
|
||||
/// <summary>
|
||||
/// Something else.
|
||||
/// </summary>
|
||||
Other,
|
||||
|
||||
/// <summary>
|
||||
/// LiveOps sections which are meant to be used after some development happened on the game.
|
||||
/// </summary>
|
||||
LiveOps
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A condition for a section to be displayed.
|
||||
/// </summary>
|
||||
public enum DisplayCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// As long as the type exists in the project, the section is shown.
|
||||
/// Exception: a section with a higher priority is defined for the same id.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// A target package is defined in TargetPackageId and the package is installed
|
||||
/// If multiple types share the same id, the one with the highest priority is shown.
|
||||
/// </summary>
|
||||
PackageInstalled,
|
||||
|
||||
/// <summary>
|
||||
/// Shown if no multiplayer package is installed (e.g. for the first time user that has not installed anything)
|
||||
/// Check SectionsFinder.k_TargetPackages to see which packages are checked
|
||||
/// </summary>
|
||||
NoPackageInstalled
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines if a section depends on a certain hosting model.
|
||||
/// </summary>
|
||||
public enum InfrastructureDependency
|
||||
{
|
||||
/// <summary>
|
||||
/// No dependency, the section is shown if all other conditions are also met.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Only available when the user has selected a client hosted infrastructure.
|
||||
/// </summary>
|
||||
ClientHosted,
|
||||
|
||||
/// <summary>
|
||||
/// Only available when the user has selected a dedicated server infrastructure.
|
||||
/// </summary>
|
||||
DedicatedServer,
|
||||
|
||||
/// <summary>
|
||||
/// Only available when the user has selected Cloud Code as their hosting model.
|
||||
/// </summary>
|
||||
CloudCode
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A view for a single onboarding section. Classes implementing this interface should be marked with the
|
||||
/// <see cref="OnboardingSectionAttribute"/>.
|
||||
/// </summary>
|
||||
public interface IOnboardingSection
|
||||
{
|
||||
/// <summary>
|
||||
/// The visual element that will be added to the onboarding window.
|
||||
/// After Load is called, it should never be null.
|
||||
/// </summary>
|
||||
public VisualElement Root { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Makes the section ready to be displayed.
|
||||
/// May be called several times in a row.
|
||||
/// </summary>
|
||||
public void Load();
|
||||
|
||||
/// <summary>
|
||||
/// Frees anything that needs to be.
|
||||
/// </summary>
|
||||
public void Unload();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For sections that depend on what the user selected in either the game specs or the solution selection.
|
||||
/// </summary>
|
||||
public interface ISectionDependingOnUserChoices : IOnboardingSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Receives the answer data and handles it. This is called after Load.
|
||||
/// </summary>
|
||||
/// <param name="answerData">The latest value of answerData</param>
|
||||
public void HandleAnswerData(AnswerData answerData) { }
|
||||
|
||||
/// <summary>
|
||||
/// Receives the user selection data and handles it. This is called after Load.
|
||||
/// </summary>
|
||||
/// <param name="selectedSolutionsData">The latest value of the selection</param>
|
||||
public void HandleUserSelectionData(SelectedSolutionsData selectedSolutionsData) { }
|
||||
|
||||
/// <summary>
|
||||
/// Receives the preset data and handles it. This is called after Load.
|
||||
/// </summary>
|
||||
/// <param name="preset">The latest preset value</param>
|
||||
public void HandlePreset(Preset preset) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implement this interface to have access to the Multiplayer Center analytics provider.
|
||||
/// Use the analytics provider to log events when the user interacts with the section.
|
||||
/// </summary>
|
||||
public interface ISectionWithAnalytics
|
||||
{
|
||||
/// <summary>
|
||||
/// This will be set before the load function is called.
|
||||
/// The implementor can then use the Analytics provider to send events to the analytics backend.
|
||||
/// </summary>
|
||||
public IOnboardingSectionAnalyticsProvider AnalyticsProvider { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bd8e2f47a6c4478ab8f0ade7687e79b
|
||||
timeCreated: 1700730627
|
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Common.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of interaction that the user has with a button in the getting started tab.
|
||||
/// </summary>
|
||||
public enum InteractionDataType
|
||||
{
|
||||
/// <summary>
|
||||
/// For a button that does something in the editor, e.g. a button that opens a window or imports a sample.
|
||||
/// </summary>
|
||||
CallToAction = 0,
|
||||
|
||||
/// <summary>
|
||||
/// For a button that opens a URL in the browser (e.g. a documentation link).
|
||||
/// </summary>
|
||||
Link = 1,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For the object that provides the analytics functionality to send interaction events on some Onboarding section
|
||||
/// in the getting started tab.
|
||||
/// </summary>
|
||||
public interface IOnboardingSectionAnalyticsProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Send event for a button interaction in the getting started tab.
|
||||
/// </summary>
|
||||
/// <param name="type"> Whether it is a call to action or a link</param>
|
||||
/// <param name="displayName"> The name of the button in the UI</param>
|
||||
void SendInteractionEvent(InteractionDataType type, string displayName);
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 833328f3bcf144cc94f92e286e86b427
|
||||
timeCreated: 1714381093
|
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Game genres that can be selected. Each one is associated with pre-selected answers for the questionnaire,
|
||||
/// except for `None`.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[InspectorOrder()] // will sort the values alphabetically in the inspector
|
||||
public enum Preset
|
||||
{
|
||||
/// <summary>
|
||||
/// Start from scratch, no preset.
|
||||
/// </summary>
|
||||
[InspectorName("-")]
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Adventure genre.
|
||||
/// </summary>
|
||||
[InspectorName("Adventure")]
|
||||
Adventure,
|
||||
|
||||
/// <summary>
|
||||
/// Shooter, Battle Royale, Battle Arena genre.
|
||||
/// </summary>
|
||||
[InspectorName("Shooter, Battle Royale, Battle Arena")]
|
||||
Shooter,
|
||||
|
||||
/// <summary>
|
||||
/// Racing genre.
|
||||
/// </summary>
|
||||
[InspectorName("Racing")]
|
||||
Racing,
|
||||
|
||||
/// <summary>
|
||||
/// Card Battle, Turn-based, Tabletop genre.
|
||||
/// </summary>
|
||||
[InspectorName("Card Battle, Turn-based, Tabletop")]
|
||||
TurnBased,
|
||||
|
||||
/// <summary>
|
||||
/// Simulation genre.
|
||||
/// </summary>
|
||||
[InspectorName("Simulation")]
|
||||
Simulation,
|
||||
|
||||
/// <summary>
|
||||
/// Strategy genre.
|
||||
/// </summary>
|
||||
[InspectorName("Strategy")]
|
||||
Strategy,
|
||||
|
||||
/// <summary>
|
||||
/// Sports genre.
|
||||
/// </summary>
|
||||
[InspectorName("Sports")]
|
||||
Sports,
|
||||
|
||||
/// <summary>
|
||||
/// Role-Playing, MMO genre.
|
||||
/// </summary>
|
||||
[InspectorName("Role-Playing, MMO")]
|
||||
RolePlaying,
|
||||
|
||||
/// <summary>
|
||||
/// Async, Idle, Hyper Casual, Puzzle genre.
|
||||
/// </summary>
|
||||
[InspectorName("Async, Idle, Hyper Casual, Puzzle")]
|
||||
Async,
|
||||
|
||||
/// <summary>
|
||||
/// Fighting genre.
|
||||
/// </summary>
|
||||
[InspectorName("Fighting")]
|
||||
Fighting,
|
||||
|
||||
/// <summary>
|
||||
/// Arcade, Platformer, Sandbox genre.
|
||||
/// </summary>
|
||||
[InspectorName("Arcade, Platformer, Sandbox")]
|
||||
Sandbox
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a9ab22e4e004c7db707338bccb538b9
|
||||
timeCreated: 1713777478
|
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the selection of the main solutions from the recommendation tab.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class SelectedSolutionsData
|
||||
{
|
||||
/// <summary>
|
||||
/// The possible hosting models that the user can select.
|
||||
/// </summary>
|
||||
public enum HostingModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Empty (no selection)
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Client hosted model
|
||||
/// </summary>
|
||||
ClientHosted,
|
||||
|
||||
/// <summary>
|
||||
/// Dedicated server model
|
||||
/// </summary>
|
||||
DedicatedServer,
|
||||
|
||||
/// <summary>
|
||||
/// Most of the logic will be in the cloud.
|
||||
/// </summary>
|
||||
CloudCode,
|
||||
|
||||
/// <summary>
|
||||
/// Distributed Authority (the authority over the game logic is spread across multiple clients).
|
||||
/// </summary>
|
||||
DistributedAuthority,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The possible netcode solutions that the user can select.
|
||||
/// </summary>
|
||||
public enum NetcodeSolution
|
||||
{
|
||||
/// <summary>
|
||||
/// Empty (no selection)
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Netcode for GameObjects
|
||||
/// </summary>
|
||||
NGO,
|
||||
|
||||
/// <summary>
|
||||
/// Netcode for Entities
|
||||
/// </summary>
|
||||
N4E,
|
||||
|
||||
/// <summary>
|
||||
/// Custom netcode solution, potentially based on Unity Transport
|
||||
/// </summary>
|
||||
CustomNetcode,
|
||||
|
||||
/// <summary>
|
||||
/// No netcode (no real time synchronization needed)
|
||||
/// </summary>
|
||||
NoNetcode
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The hosting model selected by the user.
|
||||
/// </summary>
|
||||
public HostingModel SelectedHostingModel;
|
||||
|
||||
/// <summary>
|
||||
/// The netcode solution selected by the user.
|
||||
/// </summary>
|
||||
public NetcodeSolution SelectedNetcodeSolution;
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec737041edec4a5b93f7953a6ffe9f06
|
||||
timeCreated: 1711027351
|
@@ -0,0 +1,69 @@
|
||||
namespace Unity.Multiplayer.Center.Onboarding
|
||||
{
|
||||
/// <summary>
|
||||
/// Common style classes that can be accessed from the quickstart sections
|
||||
/// </summary>
|
||||
public static class StyleConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Green checkmark to reflect a success
|
||||
/// Applicable to <c>VisualElement</c> to show a success state
|
||||
/// </summary>
|
||||
public const string CheckmarkClass = "checkmark-icon";
|
||||
|
||||
/// <summary>
|
||||
/// The style that enables to have foldable sections in the getting started tab.
|
||||
/// Applicable to <c>Foldout</c>
|
||||
/// </summary>
|
||||
public const string OnBoardingSectionFoldout = "section-foldout";
|
||||
|
||||
/// <summary>
|
||||
/// Default style for an onboarding section
|
||||
/// Apply to the top level element (Root) in Onboarding section
|
||||
/// Applicable to <c>VisualElement</c>
|
||||
/// </summary>
|
||||
public const string OnBoardingSectionClass = "onboarding-section";
|
||||
|
||||
/// <summary>
|
||||
/// Default style for the title of an onboarding section
|
||||
/// Apply to the title visual element (<c>Label</c>) in Onboarding section
|
||||
/// </summary>
|
||||
public const string OnboardingSectionTitle = "onboarding-section-title";
|
||||
|
||||
/// <summary>
|
||||
/// Button inside the header of a onboarding-section
|
||||
/// Applicable to <c>Button</c>
|
||||
/// </summary>
|
||||
public const string OnBoardingSectionMainButton = "onboarding-section-mainbutton";
|
||||
|
||||
/// <summary>
|
||||
/// Button inside the header of a onboarding-section
|
||||
/// Applicable to <c>Label</c>
|
||||
/// </summary>
|
||||
public const string OnBoardingShortDescription = "onboarding-section-short-description";
|
||||
|
||||
/// <summary>
|
||||
/// A button that opens a documentation page
|
||||
/// Applicable to <c>Button</c>
|
||||
/// </summary>
|
||||
public const string DocButtonClass = "doc-button";
|
||||
|
||||
/// <summary>
|
||||
/// An element that will take all the remaining space in a flex container
|
||||
/// Applicable to <c>VisualElement</c>
|
||||
/// </summary>
|
||||
public const string FlexSpaceClass = "flex-spacer";
|
||||
|
||||
/// <summary>
|
||||
/// Horizontal flex container
|
||||
/// Applicable to <c>VisualElement</c>
|
||||
/// </summary>
|
||||
public const string HorizontalContainerClass = "horizontal-container";
|
||||
|
||||
/// <summary>
|
||||
/// Darker background color in dark mode, lighter in light mode
|
||||
/// Applicable to <c>VisualElement</c>
|
||||
/// </summary>
|
||||
public const string HighlightBackgroundClass = "highlight-background-color";
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50d5ff9fe4ab4477b7ea648da183e9d1
|
||||
timeCreated: 1701879951
|
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "Unity.Multiplayer.Center.Common"
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84abd2ab34a74600a33a3bb9d72859fe
|
||||
timeCreated: 1700734156
|
@@ -0,0 +1,3 @@
|
||||
* [About the Multiplayer Center package](index.md)
|
||||
* [System requirements](sys-req)
|
||||
* [Use the Multiplayer Center window](use-multiplayer-center)
|
@@ -0,0 +1,7 @@
|
||||
# Multiplayer Center package
|
||||
|
||||
The Multiplayer Center package generates a customized list of Unity packages and services for the type of multiplayer game you want to create and installs them.
|
||||
|
||||
## Install
|
||||
|
||||
To install this package, refer to [Install a package from a registry](https://docs.unity3d.com/Manual/upm-ui-install.html).
|
@@ -0,0 +1,4 @@
|
||||
# System requirements
|
||||
|
||||
The Unity Multiplayer Center package is compatible with the following versions of the Unity Editor:
|
||||
- Unity 6 beta and later
|
@@ -0,0 +1,43 @@
|
||||
# Use the Multiplayer Center window
|
||||
|
||||
Open the Unity Multiplayer Center window by selecting **Window** > **Multiplayer** > **Multiplayer Center**.
|
||||
To use the Multiplayer Center, do the following:
|
||||
|
||||
1. Select **Game Specifications** to generate recommended packages and solutions in the **Recommendation** tab
|
||||
2. Install the packages the Multiplayer Center recommends.
|
||||
3. Follow the examples and resources in the **Quickstart** tab to use the packages you installed.
|
||||
|
||||
## Generate a list of recommended packages for a multiplayer game
|
||||
|
||||
To generate a list of packages that meet the needs of the multiplayer game you want to make, do the following:
|
||||
|
||||
1. Select a value in each **Game Specification** field.
|
||||
2. Examine the packages that the Multiplayer Center suggests are the best solutions for your game.
|
||||
|
||||
Hover over each package name in the **Recommended Multiplayer Packages** section to learn why each package is useful for the game specifications you selected.
|
||||
|
||||
The recommendations that appear in the **Recommendation** tab change immediately when you change any of the **Game Specifications** properties.
|
||||
|
||||
You can select which packages to install or ignore. However, you can't deselect packages that are essential for the netcode solution or hosting model you selected.
|
||||
|
||||
## Install recommended packages
|
||||
|
||||
To install the packages the Multiplayer Center recommended, select **Install Packages**. Unity then installs these packages and their dependencies in your project and automatically opens the **Quickstart** tab.
|
||||
To remove a package from the install process, deselect the checkbox next to the package name before you select **Install Packages**.
|
||||
|
||||
When a tick appears next to a package name, hover over it to learn which version of this package exists in your project.
|
||||
|
||||
To install an individual package:
|
||||
1. Select the Open Package Manager icon next to the package you want to install.
|
||||
2. In the Package Manager window that appears, select **Install**.
|
||||
|
||||
**Important**: If the Multiplayer Center detects that any other multiplayer package is already installed, a warning dialog appears to cancel the installation of those packages. To avoid breaking changes, backup your project before you continue. If you continue with the installation, Unity installs or upgrades the selected packages but doesn't remove existing packages. This can cause compatibility issues. To fix them, remove the conflicting packages from your project in the [Package Manager window](https://docs.unity3d.com/Manual/upm-ui.html).
|
||||
|
||||
## Get started with the recommended packages
|
||||
|
||||
The **Quickstart** tab includes guidance, examples, and links to resources about the packages that the Multiplayer Center installs. Follow this guidance to set up and use these packages. The **Quickstart** content sometimes changes based on the recommendations you selected in the **Recommendation** tab.
|
||||
|
||||
The Quickstart tab organises this guidance into categories. Follow the instructions in each category from top to bottom to set up each package in your project.
|
||||
|
||||
Some packages have the option to import example setups to help you get started with their functionality. To modify a script asset that the Multiplayer Center imports, copy the contents of the script to a new file and change the namespace.
|
||||
|
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72aad6ae0dafb492cbf852432441bb38
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21f32d2f4add49b3b11fadb6889a2156
|
||||
timeCreated: 1714382287
|
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using Unity.Multiplayer.Center.Common.Analytics;
|
||||
using UnityEngine.Analytics;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Package representation in the analytics data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal struct Package
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier of the package.
|
||||
/// </summary>
|
||||
public string PackageId;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user has selected this package for installation.
|
||||
/// </summary>
|
||||
public bool SelectedForInstall;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the package was recommended.
|
||||
/// </summary>
|
||||
public bool IsRecommended;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the package was already installed when the installation attempt event occured
|
||||
/// </summary>
|
||||
public bool IsAlreadyInstalled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single Answer to the GameSpecs questionnaire.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal struct GameSpec
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier of the answered question (does not change).
|
||||
/// </summary>
|
||||
public string QuestionId;
|
||||
|
||||
/// <summary>
|
||||
/// The text of the question as displayed in the UI (may change with versions).
|
||||
/// </summary>
|
||||
public string QuestionText;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the question accepts multiple answers.
|
||||
/// </summary>
|
||||
public bool AcceptsMultipleAnswers;
|
||||
|
||||
/// <summary>
|
||||
/// The identifier of the answered question (does not change).
|
||||
/// </summary>
|
||||
public string AnswerId;
|
||||
|
||||
/// <summary>
|
||||
/// The text of the answer as displayed in the UI (may change with versions).
|
||||
/// </summary>
|
||||
public string AnswerText;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal struct RecommendationData : IAnalytic.IData
|
||||
{
|
||||
/// <summary>
|
||||
/// The preset selected by the user.
|
||||
/// </summary>
|
||||
public int Preset;
|
||||
|
||||
/// <summary>
|
||||
/// The preset selected by the user (game genre) as displayed in the UI.
|
||||
/// </summary>
|
||||
public string PresetName;
|
||||
|
||||
/// <summary>
|
||||
/// The version defined in the Questionnaire data.
|
||||
/// </summary>
|
||||
public string QuestionnaireVersion;
|
||||
|
||||
/// <summary>
|
||||
/// All the selected answers to the questions of the game specs questionnaire.
|
||||
/// </summary>
|
||||
public GameSpec[] GameSpecs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What type of content the user Interacted with (buttons).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal struct InteractionData : IAnalytic.IData
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier of the section that contains the button.
|
||||
/// </summary>
|
||||
public string SectionId;
|
||||
|
||||
/// <summary>
|
||||
/// Whether it is a call to action or a link.
|
||||
/// </summary>
|
||||
public InteractionDataType Type;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the button in the UI.
|
||||
/// </summary>
|
||||
public string DisplayName;
|
||||
|
||||
/// <summary>
|
||||
/// The target package for which the section is helpful.
|
||||
/// </summary>
|
||||
public string TargetPackageId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Payload of the installation event.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal struct InstallData : IAnalytic.IData
|
||||
{
|
||||
/// <summary>
|
||||
/// The preset selected by the user.
|
||||
/// </summary>
|
||||
public int Preset;
|
||||
|
||||
/// <summary>
|
||||
/// The preset selected by the user (game genre) as displayed in the UI.
|
||||
/// </summary>
|
||||
public string PresetName;
|
||||
|
||||
/// <summary>
|
||||
/// The version defined in the Questionnaire data.
|
||||
/// </summary>
|
||||
public string QuestionnaireVersion;
|
||||
|
||||
/// <summary>
|
||||
/// All the selected answers to the questions of the game specs questionnaire.
|
||||
/// </summary>
|
||||
public GameSpec[] GamesSpecs;
|
||||
|
||||
/// <summary>
|
||||
/// The packages that were in the recommendation tab of the multiplayer center
|
||||
/// </summary>
|
||||
public Package[] Packages;
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cee1dc929764056ac40ece91efef712
|
||||
timeCreated: 1714481696
|
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Unity.Multiplayer.Center.Common;
|
||||
using Unity.Multiplayer.Center.Questionnaire;
|
||||
using Unity.Multiplayer.Center.Recommendations;
|
||||
using Unity.Multiplayer.Center.Window.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Analytics
|
||||
{
|
||||
internal static class AnalyticsUtils
|
||||
{
|
||||
// hard-coded to avoid recomputing every time / resizing arrays
|
||||
public const int NumNetcodePackage = 2;
|
||||
public const int NumHostingPackages = 1;
|
||||
|
||||
/// <summary>
|
||||
/// From the recommendation view data (which contains the packages that the user sees and the user's selection),
|
||||
/// create the list of packages that will be sent to the analytics backend.
|
||||
/// </summary>
|
||||
/// <param name="data">The recommendation view data as shown in the recommendation tab</param>
|
||||
/// <param name="solutionToPackageData">The packages views</param>
|
||||
/// <returns>The list of packages to be sent along with the installation event.</returns>
|
||||
public static Package[] GetPackagesWithAnalyticsFormat(RecommendationViewData data, SolutionsToRecommendedPackageViewData solutionToPackageData)
|
||||
{
|
||||
var selectedNetcode = RecommendationUtils.GetSelectedNetcode(data);
|
||||
var selectedHostingModel = RecommendationUtils.GetSelectedHostingModel(data);
|
||||
var packages = solutionToPackageData.GetPackagesForSelection(selectedNetcode.Solution, selectedHostingModel.Solution);
|
||||
var packageCount = NumNetcodePackage + NumHostingPackages + packages.Length;
|
||||
|
||||
var result = new Package[packageCount];
|
||||
var resultIndex = 0;
|
||||
|
||||
AddSolutionPackages(data.NetcodeOptions, result, ref resultIndex);
|
||||
AddSolutionPackages(data.ServerArchitectureOptions, result, ref resultIndex);
|
||||
AddRecommendedPackages(packages, result, ref resultIndex);
|
||||
|
||||
Debug.Assert(resultIndex == packageCount, $"Expected {packageCount} packages, got {resultIndex}");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all the inspector name attributes of the Preset enum and returns the displayNames
|
||||
/// Important! It assumes the enum values are 0, ... , N
|
||||
/// </summary>
|
||||
/// <returns>The array of preset names. The index in the array is the integer value of the enum value</returns>
|
||||
public static string[] GetPresetFullNames()
|
||||
{
|
||||
var t = typeof(Preset);
|
||||
var values = Enum.GetValues(t);
|
||||
var array = new string[values.Length];
|
||||
foreach (var value in values)
|
||||
{
|
||||
var preset = (Preset) value;
|
||||
var index = (int)preset;
|
||||
var asString = value.ToString();
|
||||
var memInfo = t.GetMember(asString);
|
||||
var attribute = memInfo[0].GetCustomAttribute<InspectorNameAttribute>(false);
|
||||
|
||||
if (attribute != null)
|
||||
{
|
||||
array[index] = attribute.displayName;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"Could not fetch the full name of the preset value {asString}");
|
||||
array[index] = asString;
|
||||
}
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts AnswerData to game specs, providing the knowledge of the display names.
|
||||
/// It assumes there is exactly one answer in the answer list at this point.
|
||||
/// </summary>
|
||||
/// <param name="data">The answer data of the user</param>
|
||||
/// <param name="answerIdToAnswerName">Mapping answer id to display name</param>
|
||||
/// <param name="questionIdToQuestionName">Mapping question id to display name</param>
|
||||
/// <returns>The list of game spec that will be consumed by the analytics backend</returns>
|
||||
public static GameSpec[] ToGameSpecs(AnswerData data,
|
||||
IReadOnlyDictionary<string, string> answerIdToAnswerName,
|
||||
IReadOnlyDictionary<string, string> questionIdToQuestionName)
|
||||
{
|
||||
var result = new GameSpec[data.Answers.Count];
|
||||
for (var i = 0; i < result.Length; ++i)
|
||||
{
|
||||
var answer = data.Answers[i];
|
||||
var answerId = answer.Answers[0]; // TODO: make sure that this always exists
|
||||
result[i] = new GameSpec()
|
||||
{
|
||||
QuestionId = answer.QuestionId,
|
||||
QuestionText = questionIdToQuestionName[answer.QuestionId],
|
||||
AcceptsMultipleAnswers = false, // TODO: add test that verifies this assumption
|
||||
AnswerId = answerId,
|
||||
AnswerText = answerIdToAnswerName[answerId]
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the mapping from question id to question display name
|
||||
/// </summary>
|
||||
/// <param name="questionnaireData">The questionnaire data</param>
|
||||
/// <returns>The mapping</returns>
|
||||
public static IReadOnlyDictionary<string, string> GetQuestionDisplayNames(QuestionnaireData questionnaireData)
|
||||
{
|
||||
var dictionary = new Dictionary<string, string>();
|
||||
foreach (var question in questionnaireData.Questions)
|
||||
{
|
||||
dictionary[question.Id] = question.Title;
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the mapping from answer id to answer display name
|
||||
/// </summary>
|
||||
/// <param name="questionnaireData">The questionnaire data</param>
|
||||
/// <returns>The mapping</returns>
|
||||
public static IReadOnlyDictionary<string, string> GetAnswerDisplayNames(QuestionnaireData questionnaireData)
|
||||
{
|
||||
var dictionary = new Dictionary<string, string>();
|
||||
foreach (var question in questionnaireData.Questions)
|
||||
{
|
||||
foreach (var answer in question.Choices)
|
||||
{
|
||||
dictionary[answer.Id] = answer.Title;
|
||||
}
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
static void AddSolutionPackages(RecommendedSolutionViewData[] options, Package[] result, ref int resultIndex)
|
||||
{
|
||||
foreach (var t in options)
|
||||
{
|
||||
if(string.IsNullOrEmpty(t.MainPackage?.PackageId))
|
||||
continue;
|
||||
|
||||
result[resultIndex] = new Package()
|
||||
{
|
||||
PackageId = t.MainPackage.PackageId,
|
||||
SelectedForInstall = t.Selected && t.RecommendationType != RecommendationType.Incompatible,
|
||||
IsRecommended = t.RecommendationType is RecommendationType.MainArchitectureChoice,
|
||||
IsAlreadyInstalled = t.MainPackage.IsInstalledAsProjectDependency
|
||||
};
|
||||
++resultIndex;
|
||||
}
|
||||
}
|
||||
|
||||
static void AddRecommendedPackages(RecommendedPackageViewData[] packageViewDatas, Package[] result, ref int resultIndex)
|
||||
{
|
||||
foreach (var viewData in packageViewDatas)
|
||||
{
|
||||
result[resultIndex] = new Package()
|
||||
{
|
||||
PackageId = viewData.PackageId,
|
||||
// TODO: remove hidden?
|
||||
SelectedForInstall = viewData.Selected && viewData.RecommendationType != RecommendationType.Incompatible,
|
||||
IsRecommended = viewData.RecommendationType.IsRecommendedPackage(),
|
||||
IsAlreadyInstalled = viewData.IsInstalledAsProjectDependency
|
||||
};
|
||||
++resultIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54407f5deee4439eb1885bee01956e9c
|
||||
timeCreated: 1714483618
|
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Analytics;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Does the same as the MultiplayerCenterAnalytics, but logs the events to the console instead of sending them.
|
||||
/// It is useful to debug fast, without the EditorAnalytics Debugger package (but it does not replace it).
|
||||
/// </summary>
|
||||
internal class DebugAnalytics : MultiplayerCenterAnalytics
|
||||
{
|
||||
public DebugAnalytics(string questionnaireVersion, IReadOnlyDictionary<string, string> questionDisplayNames,
|
||||
IReadOnlyDictionary<string,string> answerDisplayNames)
|
||||
: base(questionnaireVersion, questionDisplayNames, answerDisplayNames) { }
|
||||
|
||||
protected override void SendAnalytic(IAnalytic analytic)
|
||||
{
|
||||
analytic.TryGatherData(out var data, out var _);
|
||||
switch (data)
|
||||
{
|
||||
case InstallData installData:
|
||||
Debug.Log($"Event: {analytic.GetType()} - Data: {ToString(installData)}");
|
||||
break;
|
||||
case RecommendationData recommendationData:
|
||||
Debug.Log($"Event: {analytic.GetType()} - Data: {ToString(recommendationData)}");
|
||||
break;
|
||||
case InteractionData interactionEventAnalytic:
|
||||
Debug.Log($"Event: {analytic.GetType()} - Data: {ToString(interactionEventAnalytic)}");
|
||||
break;
|
||||
default:
|
||||
Debug.Log($"Unknown event: {analytic.GetType()} - Data: {data}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static string ToString(GameSpec p) => $"GameSpec [{p.QuestionText} -> {p.AnswerText}]";
|
||||
|
||||
static string ToString(Package p) => $"Package [{p.PackageId} - Selected {p.SelectedForInstall} - Reco {p.IsRecommended} - Inst {p.IsAlreadyInstalled}]";
|
||||
|
||||
static string ToString(InstallData data)
|
||||
{
|
||||
var packageStrings = new List<string>(data.Packages.Length);
|
||||
foreach (var package in data.Packages)
|
||||
{
|
||||
packageStrings.Add(ToString(package));
|
||||
}
|
||||
return $"{data.PresetName} - Packages [{data.Packages.Length}] packages: \n{string.Join("\n", packageStrings)}";
|
||||
}
|
||||
|
||||
static string ToString(RecommendationData data)
|
||||
{
|
||||
var gameSpecStrings = new List<string>(data.GameSpecs.Length);
|
||||
foreach (var gameSpec in data.GameSpecs)
|
||||
{
|
||||
gameSpecStrings.Add(ToString(gameSpec));
|
||||
}
|
||||
return $"{data.PresetName} - GameSpecs [{data.GameSpecs.Length}] gamespecs: \n{string.Join("\n", gameSpecStrings)}";
|
||||
}
|
||||
|
||||
static string ToString(InteractionData data) => $"{data.SectionId}({data.TargetPackageId}) - {data.Type} - {data.DisplayName}";
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd20f32039084a12a8e48dffd4d58f48
|
||||
timeCreated: 1714651117
|
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Multiplayer.Center.Common;
|
||||
using Unity.Multiplayer.Center.Common.Analytics;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Analytics;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// The interface for the Multiplayer Center Analytics provider (only one functional implementation, but the
|
||||
/// interface is needed for testing purposes)
|
||||
/// </summary>
|
||||
internal interface IMultiplayerCenterAnalytics
|
||||
{
|
||||
void SendInstallationEvent(AnswerData data, Preset preset, Package[] packages);
|
||||
void SendRecommendationEvent(AnswerData data, Preset preset);
|
||||
void SendGettingStartedInteractionEvent(string targetPackageId, string sectionId, InteractionDataType type, string displayName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The concrete implementation of the multiplayer center analytics provider.
|
||||
/// It convert
|
||||
/// </summary>
|
||||
internal class MultiplayerCenterAnalytics : IMultiplayerCenterAnalytics
|
||||
{
|
||||
const string k_VendorKey = "unity.multiplayer.center";
|
||||
const string k_InstallationEventName = "multiplayer_center_onInstallClicked";
|
||||
const string k_RecommendationEventName = "multiplayer_center_onRecommendation";
|
||||
const string k_GetStartedInteractionEventName = "multiplayer_center_onGetStartedInteraction";
|
||||
|
||||
readonly string m_QuestionnaireVersion;
|
||||
readonly IReadOnlyDictionary<string, string> m_AnswerIdToAnswerName;
|
||||
readonly IReadOnlyDictionary<string, string> m_QuestionIdToQuestionName;
|
||||
readonly string[] m_PresetFullNames = AnalyticsUtils.GetPresetFullNames();
|
||||
string PresetName(Preset v) => m_PresetFullNames[(int)v];
|
||||
|
||||
GameSpec[] FillGameSpecs(AnswerData data)
|
||||
{
|
||||
return AnalyticsUtils.ToGameSpecs(data, m_AnswerIdToAnswerName, m_QuestionIdToQuestionName);
|
||||
}
|
||||
|
||||
protected virtual void SendAnalytic(IAnalytic analytic)
|
||||
{
|
||||
EditorAnalytics.SendAnalytic(analytic);
|
||||
}
|
||||
|
||||
public MultiplayerCenterAnalytics(string questionnaireVersion, IReadOnlyDictionary<string, string> questionDisplayNames,
|
||||
IReadOnlyDictionary<string, string> answerDisplayNames)
|
||||
{
|
||||
m_QuestionnaireVersion = questionnaireVersion;
|
||||
m_QuestionIdToQuestionName = questionDisplayNames;
|
||||
m_AnswerIdToAnswerName = answerDisplayNames;
|
||||
}
|
||||
|
||||
public void SendGettingStartedInteractionEvent(string targetPackageId, string sectionId, InteractionDataType type, string displayName)
|
||||
{
|
||||
var analytic = new GetStartedInteractionEventAnalytic(sectionId, type, displayName, targetPackageId);
|
||||
SendAnalytic(analytic);
|
||||
}
|
||||
|
||||
public void SendInstallationEvent(AnswerData data, Preset preset, Package[] packages)
|
||||
{
|
||||
var analytic = new InstallationEventAnalytic(new InstallData()
|
||||
{
|
||||
Preset = (int)preset,
|
||||
PresetName = PresetName(preset),
|
||||
QuestionnaireVersion = m_QuestionnaireVersion,
|
||||
GamesSpecs = FillGameSpecs(data),
|
||||
Packages = packages
|
||||
});
|
||||
SendAnalytic(analytic);
|
||||
}
|
||||
|
||||
public void SendRecommendationEvent(AnswerData data, Preset preset)
|
||||
{
|
||||
var analytic = new RecommendationEventAnalytic(new RecommendationData()
|
||||
{
|
||||
Preset = (int)preset,
|
||||
PresetName = PresetName(preset),
|
||||
QuestionnaireVersion = m_QuestionnaireVersion,
|
||||
GameSpecs = FillGameSpecs(data)
|
||||
});
|
||||
SendAnalytic(analytic);
|
||||
}
|
||||
|
||||
[AnalyticInfo(eventName: k_InstallationEventName, vendorKey: k_VendorKey)]
|
||||
private class InstallationEventAnalytic : IAnalytic
|
||||
{
|
||||
InstallData m_Data;
|
||||
|
||||
public InstallationEventAnalytic(InstallData data)
|
||||
{
|
||||
m_Data = data;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGatherData(out IAnalytic.IData data, out Exception error)
|
||||
{
|
||||
data = m_Data;
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[AnalyticInfo(eventName: k_RecommendationEventName, vendorKey: k_VendorKey)]
|
||||
private class RecommendationEventAnalytic : IAnalytic
|
||||
{
|
||||
RecommendationData m_Data;
|
||||
|
||||
public RecommendationEventAnalytic(RecommendationData data)
|
||||
{
|
||||
m_Data = data;
|
||||
}
|
||||
|
||||
public bool TryGatherData(out IAnalytic.IData data, out Exception error)
|
||||
{
|
||||
data = m_Data;
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[AnalyticInfo(eventName: k_GetStartedInteractionEventName, vendorKey: k_VendorKey)]
|
||||
private class GetStartedInteractionEventAnalytic : IAnalytic
|
||||
{
|
||||
InteractionData m_Data;
|
||||
|
||||
public GetStartedInteractionEventAnalytic(string sectionId, InteractionDataType type, string displayName, string targetPackageId)
|
||||
{
|
||||
m_Data = new InteractionData()
|
||||
{
|
||||
SectionId = sectionId,
|
||||
Type = type,
|
||||
DisplayName = displayName,
|
||||
TargetPackageId = targetPackageId
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGatherData(out IAnalytic.IData data, out Exception error)
|
||||
{
|
||||
data = m_Data;
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 185e678d90804a8a85ab38276eca86fb
|
||||
timeCreated: 1714382339
|
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Unity.Multiplayer.Center.Questionnaire;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Analytics
|
||||
{
|
||||
internal static class MultiplayerCenterAnalyticsFactory
|
||||
{
|
||||
public static IMultiplayerCenterAnalytics Create()
|
||||
{
|
||||
var questionnaire = QuestionnaireObject.instance;
|
||||
var questionnaireVersion = questionnaire.Questionnaire.Version;
|
||||
var questionDisplayNames = AnalyticsUtils.GetQuestionDisplayNames(questionnaire.Questionnaire);
|
||||
var answerDisplayNames = AnalyticsUtils.GetAnswerDisplayNames(questionnaire.Questionnaire);
|
||||
|
||||
// Uncomment this line to use the DebugAnalytics class instead of the MultiplayerCenterAnalytics class
|
||||
// return new DebugAnalytics(questionnaireVersion, questionDisplayNames, answerDisplayNames);
|
||||
|
||||
return new MultiplayerCenterAnalytics(questionnaireVersion, questionDisplayNames, answerDisplayNames);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bce0b24c28e45d884acfcf49a3b8945
|
||||
timeCreated: 1714640827
|
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using Unity.Multiplayer.Center.Common.Analytics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// The concrete implementation of the IOnboardingSectionAnalyticsProvider interface.
|
||||
/// It shall be created by the GettingStarted tab with the knowledge of the target package and the section id
|
||||
/// provided by the attribute of the onboarding section, so that the section implementer does not have to worry
|
||||
/// about it.
|
||||
/// </summary>
|
||||
internal class OnboardingSectionAnalyticsProvider : IOnboardingSectionAnalyticsProvider
|
||||
{
|
||||
readonly IMultiplayerCenterAnalytics m_Analytics;
|
||||
readonly string m_TargetPackageId;
|
||||
readonly string m_SectionId;
|
||||
|
||||
public OnboardingSectionAnalyticsProvider(IMultiplayerCenterAnalytics analytics, string targetPackageId, string sectionId)
|
||||
{
|
||||
Debug.Assert(analytics != null);
|
||||
m_Analytics = analytics;
|
||||
m_TargetPackageId = targetPackageId;
|
||||
m_SectionId = sectionId;
|
||||
}
|
||||
|
||||
public void SendInteractionEvent(InteractionDataType type, string displayName)
|
||||
{
|
||||
m_Analytics.SendGettingStartedInteractionEvent(m_TargetPackageId, m_SectionId, type, displayName);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9faa4a7023c43c5805642250c703f48
|
||||
timeCreated: 1714637861
|
@@ -0,0 +1,5 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly:InternalsVisibleTo("Unity.Multiplayer.Center.Editor.Tests")]
|
||||
[assembly:InternalsVisibleTo("Unity.Multiplayer.Center.Integrations")]
|
||||
[assembly:InternalsVisibleTo("Unity.Multiplayer.Center.GettingStartedTab.Tests")]
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51566b3736df4282bbe76645014b0cc5
|
||||
timeCreated: 1700487414
|
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22d3dbf8d488d49d2b1130d698010dee
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,325 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Multiplayer.Center
|
||||
{
|
||||
internal static class PackageManagement
|
||||
{
|
||||
static PackageInstaller s_Installer;
|
||||
|
||||
/// <summary>
|
||||
/// Opens the package manager window with selected package name and hides error
|
||||
/// </summary>
|
||||
public static void OpenPackageManager(string packageName)
|
||||
{
|
||||
try
|
||||
{
|
||||
UnityEditor.PackageManager.UI.Window.Open(packageName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Hide the error in the PackageManager API until the team fixes it
|
||||
// Debug.Log("Error opening Package Manager: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the package is a direct dependency of the project
|
||||
/// </summary>
|
||||
/// <param name="packageId">The package name/id e.g. com.unity.netcode</param>
|
||||
/// <returns>True if the package is a direct dependency</returns>
|
||||
public static bool IsDirectDependency(string packageId)
|
||||
{
|
||||
var package = GetInstalledPackage(packageId);
|
||||
return package != null && package.isDirectDependency;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a package is installed.
|
||||
/// </summary>
|
||||
/// <param name="packageId">The package name, e.g. com.unity.netcode</param>
|
||||
/// <returns>True if the package is installed, false otherwise</returns>
|
||||
public static bool IsInstalled(string packageId) => GetInstalledPackage(packageId) != null;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a package is embedded, linked locally, installed via Git or local Tarball.
|
||||
/// </summary>
|
||||
/// <param name="packageId">The package name, e.g. com.unity.netcode</param>
|
||||
/// <returns>True if the package is linked locally, false otherwise</returns>
|
||||
public static bool IsLinkedLocallyOrEmbeddedOrViaGit(string packageId) =>
|
||||
GetInstalledPackage(packageId) is { source: PackageSource.Embedded or PackageSource.Local or PackageSource.Git or PackageSource.LocalTarball };
|
||||
|
||||
/// <summary>
|
||||
/// Finds the installed package with the given packageId or returns null.
|
||||
/// </summary>
|
||||
/// <param name="packageId">The package name/id e.g. com.unity.netcode</param>
|
||||
/// <returns>The package info</returns>
|
||||
public static UnityEditor.PackageManager.PackageInfo GetInstalledPackage(string packageId)
|
||||
{
|
||||
return UnityEditor.PackageManager.PackageInfo.FindForPackageName(packageId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters out the packages that are already embedded, linked locally, installed via Git or local Tarball and returns this new list.
|
||||
/// </summary>
|
||||
/// <param name="installCandidates">A list of package IDs that are candidates for installation.</param>
|
||||
/// <returns>A new filtered list of packages.</returns>
|
||||
public static IEnumerable<string> RemoveLocallyLinkedOrEmbeddedOrViaGitPackagesFromList(IEnumerable<string> installCandidates)
|
||||
{
|
||||
var filteredList = new List<string>();
|
||||
|
||||
foreach (var packageId in installCandidates)
|
||||
{
|
||||
if (!IsLinkedLocallyOrEmbeddedOrViaGit(packageId))
|
||||
{
|
||||
filteredList.Add(packageId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"Removing {packageId} from install candidates.\n" +
|
||||
"This package is already embedded, linked locally, installed via Git, or from a local tarball. " +
|
||||
"Please check the Package Manager for more information or to upgrade manually.");
|
||||
}
|
||||
}
|
||||
|
||||
return filteredList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if any of the given packageIds is installed.
|
||||
/// </summary>
|
||||
/// <param name="packageIds">List of package is e.g com.unity.netcode</param>
|
||||
/// <returns>True if any package is installed, false otherwise</returns>
|
||||
public static bool IsAnyPackageInstalled(params string[] packageIds)
|
||||
{
|
||||
var installedPackages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages();
|
||||
var hashset = new HashSet<string>();
|
||||
|
||||
foreach (var package in installedPackages)
|
||||
{
|
||||
hashset.Add(package.name);
|
||||
}
|
||||
|
||||
foreach (var packageId in packageIds)
|
||||
{
|
||||
if (hashset.Contains(packageId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a single package and invokes the callback when the package is installed/when the install failed.
|
||||
/// </summary>
|
||||
/// <param name="packageId">The package name/id e.g. com.unity.netcode</param>
|
||||
/// <param name="onInstalled">The callback</param>
|
||||
public static void InstallPackage(string packageId, Action<bool> onInstalled = null)
|
||||
{
|
||||
s_Installer = new PackageInstaller(packageId);
|
||||
s_Installer.OnInstalled += onInstalled;
|
||||
s_Installer.OnInstalled += _ => s_Installer = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register to an existing installation callback. This has no effect if no installation is ongoing (check
|
||||
/// <see cref="IsInstallationFinished"/> to see if that is the case).
|
||||
/// </summary>
|
||||
/// <param name="onInstalled">The callback</param>
|
||||
public static void RegisterToExistingInstaller(Action<bool> onInstalled)
|
||||
{
|
||||
if (s_Installer != null)
|
||||
{
|
||||
s_Installer.OnInstalled += onInstalled;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs several packages and invokes the callback when all packages are installed/when the installation failed.
|
||||
/// </summary>
|
||||
/// <param name="packageIds">The package names/ids e.g. com.unity.netcode</param>
|
||||
/// <param name="onAllInstalled">The callback</param>
|
||||
/// <param name="packageIdsToRemove">Optional package name/ids to remove</param>
|
||||
public static void InstallPackages(IEnumerable<string> packageIds, Action<bool> onAllInstalled = null, IEnumerable<string> packageIdsToRemove = null)
|
||||
{
|
||||
s_Installer = new PackageInstaller(RemoveLocallyLinkedOrEmbeddedOrViaGitPackagesFromList(packageIds), packageIdsToRemove);
|
||||
s_Installer.OnInstalled += onAllInstalled;
|
||||
s_Installer.OnInstalled += _ => s_Installer = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a dictionary with package names as keys and versions as values
|
||||
/// </summary>
|
||||
/// <returns>The mapping (package id, installed version) </returns>
|
||||
internal static Dictionary<string, string> InstalledPackageDictionary()
|
||||
{
|
||||
var installedPackages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages();
|
||||
var installedPackageDictionary = new Dictionary<string, string>();
|
||||
|
||||
foreach (var package in installedPackages)
|
||||
{
|
||||
var splitPackageId = package.packageId.Split('@');
|
||||
if (splitPackageId.Length == 2)
|
||||
{
|
||||
installedPackageDictionary[splitPackageId[0]] = splitPackageId[1];
|
||||
}
|
||||
}
|
||||
|
||||
return installedPackageDictionary;
|
||||
}
|
||||
|
||||
internal class VersionChecker
|
||||
{
|
||||
SearchRequest m_Request;
|
||||
public VersionChecker(string packageID)
|
||||
{
|
||||
m_Request = Client.Search(packageID, false);
|
||||
EditorApplication.update += Progress;
|
||||
}
|
||||
|
||||
public event Action<UnityEditor.PackageManager.PackageInfo> OnVersionFound;
|
||||
|
||||
void Progress()
|
||||
{
|
||||
if (!m_Request.IsCompleted) return;
|
||||
|
||||
EditorApplication.update -= Progress;
|
||||
var foundPackage = m_Request.Result;
|
||||
foreach (var packageInfo in foundPackage)
|
||||
{
|
||||
OnVersionFound?.Invoke(packageInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PackageInstaller
|
||||
{
|
||||
Request m_Request;
|
||||
string[] m_PackagesToAddIds;
|
||||
public event Action<bool> OnInstalled;
|
||||
|
||||
public PackageInstaller(string packageId)
|
||||
{
|
||||
// Add a package to the project
|
||||
m_Request = Client.Add(packageId);
|
||||
m_PackagesToAddIds = new[] {packageId};
|
||||
EditorApplication.update += Progress;
|
||||
}
|
||||
|
||||
public PackageInstaller(IEnumerable<string> packageIds, IEnumerable<string> packageIdsToRemove = null)
|
||||
{
|
||||
var packageIdsList = new List<string>();
|
||||
foreach (var id in packageIds)
|
||||
{
|
||||
packageIdsList.Add(id);
|
||||
}
|
||||
|
||||
var packageIdsArray = packageIdsList.ToArray();
|
||||
|
||||
string[] packageIdsToRemoveArray = null;
|
||||
if (packageIdsToRemove != null)
|
||||
{
|
||||
var packageIdsToRemoveList = new List<string>();
|
||||
foreach (var id in packageIdsToRemove)
|
||||
{
|
||||
packageIdsToRemoveList.Add(id);
|
||||
}
|
||||
packageIdsToRemoveArray = packageIdsToRemoveList.ToArray();
|
||||
}
|
||||
|
||||
// Add a package to the project
|
||||
m_Request = Client.AddAndRemove(packageIdsArray, packageIdsToRemoveArray);
|
||||
m_PackagesToAddIds = packageIdsArray;
|
||||
EditorApplication.update += Progress;
|
||||
}
|
||||
|
||||
public bool IsCompleted()
|
||||
{
|
||||
return m_Request == null || m_Request.IsCompleted;
|
||||
}
|
||||
|
||||
void Progress()
|
||||
{
|
||||
if (!m_Request.IsCompleted) return;
|
||||
|
||||
EditorApplication.update -= Progress;
|
||||
if (m_Request.Status == StatusCode.Success)
|
||||
{
|
||||
Debug.Log("Installed: " + GetInstalledPackageId());
|
||||
}
|
||||
else if (m_Request.Status >= StatusCode.Failure)
|
||||
{
|
||||
// if the request has more than one package, it will only prompt error message for one
|
||||
// We should prompt all the failed packages
|
||||
Debug.Log("Package installation request with selected packages: " + String.Join(", ", m_PackagesToAddIds) +
|
||||
" failed. \n Reason: "+ m_Request.Error.message);
|
||||
}
|
||||
|
||||
OnInstalled?.Invoke(m_Request.Status == StatusCode.Success);
|
||||
}
|
||||
|
||||
string GetInstalledPackageId()
|
||||
{
|
||||
switch (m_Request)
|
||||
{
|
||||
case AddRequest addRequest:
|
||||
return addRequest.Result.packageId;
|
||||
case AddAndRemoveRequest addAndRemoveRequest:
|
||||
var packageIds = new List<string>();
|
||||
foreach (var packageInfo in addAndRemoveRequest.Result)
|
||||
{
|
||||
packageIds.Add(packageInfo.packageId);
|
||||
}
|
||||
return string.Join(", ", packageIds);
|
||||
default:
|
||||
throw new InvalidOperationException("Unknown request type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects if any multiplayer package is installed by checking for services and Netcode installed packages.
|
||||
/// </summary>
|
||||
/// <returns>True if any package was detected, False otherwise</returns>
|
||||
public static bool IsAnyMultiplayerPackageInstalled()
|
||||
{
|
||||
var packagesToCheck = new []
|
||||
{
|
||||
"com.unity.netcode",
|
||||
"com.unity.netcode.gameobjects",
|
||||
"com.unity.services.multiplayer",
|
||||
"com.unity.transport",
|
||||
"com.unity.dedicated-server",
|
||||
"com.unity.services.cloudcode",
|
||||
"com.unity.multiplayer.playmode",
|
||||
"com.unity.services.vivox"
|
||||
// Note about "com.unity.services.core": it used to be installed only with multiplayer packages, but it is also a dependency of the analytics, which is now always installed.
|
||||
};
|
||||
|
||||
foreach (var package in packagesToCheck)
|
||||
{
|
||||
if (IsInstalled(package))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the installation process has finished.
|
||||
/// </summary>
|
||||
/// <returns>True if there is no current installer instance or installation is finished on the installer</returns>
|
||||
public static bool IsInstallationFinished()
|
||||
{
|
||||
return s_Installer == null || s_Installer.IsCompleted();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 676a3852811f4199813ce7f6dbac6880
|
||||
timeCreated: 1694784710
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a7dcde6448847648629a13d746ce966
|
||||
timeCreated: 1695036776
|
@@ -0,0 +1,225 @@
|
||||
using System;
|
||||
using Unity.Multiplayer.Center.Analytics;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Window
|
||||
{
|
||||
internal class MultiplayerCenterWindow : EditorWindow, ISerializationCallbackReceiver
|
||||
{
|
||||
const string k_PathInPackage = "Packages/com.unity.multiplayer.center/Editor/MultiplayerCenterWindow";
|
||||
const string k_SpinnerClassName = "processing";
|
||||
const string k_SessionStateDomainReloadKey = "MultiplayerCenter.InDomainReload";
|
||||
|
||||
VisualElement m_SpinningIcon;
|
||||
|
||||
/// <summary>
|
||||
/// Nest the main container in a VisualElement to allow for easy enabling/disabling of the entire window but
|
||||
/// without the spinning icon.
|
||||
/// </summary>
|
||||
VisualElement m_MainContainer;
|
||||
|
||||
Vector2 m_WindowSize = new(350, 300);
|
||||
|
||||
public int CurrentTab => m_TabGroup.CurrentTab;
|
||||
|
||||
// Testing purposes only. We don't want to set CurrentTab from window
|
||||
internal int CurrentTabTest
|
||||
{
|
||||
get => m_TabGroup.CurrentTab;
|
||||
set => m_TabGroup.SetSelected(value);
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
bool m_RequestGettingStartedTabAfterDomainReload = false;
|
||||
|
||||
[SerializeField]
|
||||
TabGroup m_TabGroup;
|
||||
|
||||
/// <summary>
|
||||
/// This is the reference Multiplayer Center analytics implementation. This class owns it.
|
||||
/// </summary>
|
||||
IMultiplayerCenterAnalytics m_MultiplayerCenterAnalytics;
|
||||
|
||||
IMultiplayerCenterAnalytics MultiplayerCenterAnalytics => m_MultiplayerCenterAnalytics ??= MultiplayerCenterAnalyticsFactory.Create();
|
||||
|
||||
[MenuItem("Window/Multiplayer/Multiplayer Center")]
|
||||
public static void OpenWindow()
|
||||
{
|
||||
var showUtility = false; // TODO: figure out if it would be a good idea to have a utility window (always on top, cannot be tabbed)
|
||||
GetWindow<MultiplayerCenterWindow>(showUtility, "Multiplayer Center", true);
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
// Adjust window size based on dpi scaling
|
||||
var dpiScale = EditorGUIUtility.pixelsPerPoint;
|
||||
minSize = new Vector2(m_WindowSize.x * dpiScale, m_WindowSize.y * dpiScale);
|
||||
|
||||
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeDomainReload;
|
||||
AssemblyReloadEvents.afterAssemblyReload -= OnAfterDomainReload;
|
||||
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeDomainReload;
|
||||
AssemblyReloadEvents.afterAssemblyReload += OnAfterDomainReload;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeDomainReload;
|
||||
AssemblyReloadEvents.afterAssemblyReload -= OnAfterDomainReload;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes Tab from Recommendation to the Quickstart tab.
|
||||
/// </summary>
|
||||
public void RequestShowGettingStartedTabAfterDomainReload()
|
||||
{
|
||||
m_RequestGettingStartedTabAfterDomainReload = true;
|
||||
|
||||
// If no domain reload is necessary, this will be called.
|
||||
// If domain reload is necessary, the delay call will be forgotten, but CreateGUI will be called like after any domain reload
|
||||
// An extra delay is added to make sure that the visibility conditions of the Quickstart tab have been
|
||||
// fully evaluated. This solves MTT-8939.
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
rootVisualElement.schedule.Execute(CallCreateGuiWithQuickstartRequest).ExecuteLater(300);
|
||||
};
|
||||
}
|
||||
|
||||
internal void DisableUiForInstallation()
|
||||
{
|
||||
SetSpinnerIconRotating();
|
||||
m_MainContainer.SetEnabled(false);
|
||||
}
|
||||
|
||||
internal void ReenableUiAfterInstallation()
|
||||
{
|
||||
RemoveSpinnerIconRotating();
|
||||
m_MainContainer.SetEnabled(true);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Restore the GUI if it was cleared in OnBeforeSerialize.
|
||||
if (m_TabGroup == null || m_TabGroup.ViewCount < 1)
|
||||
{
|
||||
CreateGUI();
|
||||
}
|
||||
}
|
||||
|
||||
void CreateGUI()
|
||||
{
|
||||
rootVisualElement.name = "root";
|
||||
m_MainContainer ??= new VisualElement();
|
||||
m_MainContainer.name = "recommendation-tab-container";
|
||||
m_MainContainer.Clear();
|
||||
rootVisualElement.Add(m_MainContainer);
|
||||
m_SpinningIcon = new VisualElement();
|
||||
var theme = EditorGUIUtility.isProSkin ? "dark" : "light";
|
||||
rootVisualElement.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>($"{k_PathInPackage}/UI/{theme}.uss"));
|
||||
rootVisualElement.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>($"{k_PathInPackage}/UI/MultiplayerCenterWindow.uss"));
|
||||
|
||||
if (m_TabGroup == null || m_TabGroup.ViewCount < 1 || !m_TabGroup.TabsAreValid())
|
||||
m_TabGroup = new TabGroup(MultiplayerCenterAnalytics, new ITabView[] {new RecommendationTabView(), new GettingStartedTabView()});
|
||||
else // since we are not serializing the analytics provider, we need to set it again
|
||||
m_TabGroup.MultiplayerCenterAnalytics = MultiplayerCenterAnalytics;
|
||||
|
||||
m_TabGroup.CreateTabs();
|
||||
m_MainContainer.Add(m_TabGroup.Root);
|
||||
|
||||
var installationInProgress = !PackageManagement.IsInstallationFinished();
|
||||
SetWindowContentEnabled(installationInProgress, m_RequestGettingStartedTabAfterDomainReload);
|
||||
ShowAppropriateTab(installationInProgress);
|
||||
}
|
||||
|
||||
void ShowAppropriateTab(bool installationInProgress)
|
||||
{
|
||||
if (installationInProgress)
|
||||
{
|
||||
PackageManagement.RegisterToExistingInstaller(b => RequestShowGettingStartedTabAfterDomainReload());
|
||||
m_TabGroup.SetSelected(0, force: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_RequestGettingStartedTabAfterDomainReload)
|
||||
{
|
||||
m_RequestGettingStartedTabAfterDomainReload = false;
|
||||
m_TabGroup.SetSelected(1, force: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_TabGroup.SetSelected(m_TabGroup.CurrentTab, force: true);
|
||||
}
|
||||
}
|
||||
|
||||
void SetWindowContentEnabled(bool installationInProgress, bool quickstartRequested)
|
||||
{
|
||||
m_MainContainer.SetEnabled(!installationInProgress || quickstartRequested);
|
||||
|
||||
// if we are current already processing an installation, show the spinning icon
|
||||
if (installationInProgress)
|
||||
{
|
||||
// Wait a bit because the animation does not trigger when we call this in CreateGUI
|
||||
EditorApplication.delayCall += SetSpinnerIconRotating;
|
||||
}
|
||||
|
||||
rootVisualElement.Add(m_SpinningIcon);
|
||||
}
|
||||
|
||||
void CallCreateGuiWithQuickstartRequest()
|
||||
{
|
||||
// Interestingly, setting this before registering the delay call sometimes results in the value
|
||||
// being false when CreateGUI starts, so we set it again here.
|
||||
m_RequestGettingStartedTabAfterDomainReload = true;
|
||||
CreateGUI();
|
||||
}
|
||||
|
||||
void SetSpinnerIconRotating()
|
||||
{
|
||||
m_SpinningIcon.AddToClassList(k_SpinnerClassName);
|
||||
}
|
||||
|
||||
void RemoveSpinnerIconRotating()
|
||||
{
|
||||
m_SpinningIcon?.RemoveFromClassList(k_SpinnerClassName);
|
||||
}
|
||||
|
||||
void ClearTabs()
|
||||
{
|
||||
m_TabGroup?.Clear();
|
||||
m_TabGroup = null;
|
||||
}
|
||||
|
||||
// This will not get called when the Editor is closed.
|
||||
void OnDestroy()
|
||||
{
|
||||
ClearTabs();
|
||||
}
|
||||
|
||||
static void OnBeforeDomainReload()
|
||||
{
|
||||
SessionState.SetBool(k_SessionStateDomainReloadKey, true);
|
||||
}
|
||||
|
||||
static void OnAfterDomainReload()
|
||||
{
|
||||
SessionState.SetBool(k_SessionStateDomainReloadKey, false);
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
// ClearTabs if the Window gets serialized, but we are not in DomainReload
|
||||
// This happens when the Editor closes or the WindowLayout is saved by the user.
|
||||
// This ensures that the State of the Tabs is not serialized into the WindowLayout of the User.
|
||||
if (SessionState.GetBool(k_SessionStateDomainReloadKey, false) == false)
|
||||
{
|
||||
ClearTabs();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
// Empty on purpose.
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e006510f5f4425ca971c7de0bfbb77a
|
||||
timeCreated: 1695036798
|
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
using Unity.Multiplayer.Center.Analytics;
|
||||
using Unity.Multiplayer.Center.Common;
|
||||
using Unity.Multiplayer.Center.Questionnaire;
|
||||
using Unity.Multiplayer.Center.Recommendations;
|
||||
using Unity.Multiplayer.Center.Window.UI;
|
||||
using Unity.Multiplayer.Center.Window.UI.RecommendationView;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Window
|
||||
{
|
||||
internal class RecommendationTabView : ITabView
|
||||
{
|
||||
QuestionnaireView m_QuestionnaireView;
|
||||
RecommendationView m_RecommendationView;
|
||||
|
||||
RecommendationViewBottomBar m_BottomBarView;
|
||||
|
||||
bool m_IsVisible;
|
||||
bool m_ShouldRefresh = true;
|
||||
|
||||
[SerializeField]
|
||||
PreReleaseHandling m_PreReleaseHandling = new();
|
||||
|
||||
[field: SerializeField] // Marked as redundant by Rider, but it is not.
|
||||
public string Name { get; private set; }
|
||||
|
||||
public VisualElement RootVisualElement { get; set; }
|
||||
public IMultiplayerCenterAnalytics MultiplayerCenterAnalytics { get; set; }
|
||||
|
||||
// Access to QuestionnaireView for testing purposes
|
||||
internal QuestionnaireView QuestionnaireView => m_QuestionnaireView;
|
||||
|
||||
public RecommendationTabView(string name = "Recommendation")
|
||||
{
|
||||
Name = name;
|
||||
m_PreReleaseHandling.OnAllChecksFinished += PatchData;
|
||||
m_PreReleaseHandling.CheckForUpdates();
|
||||
Undo.undoRedoPerformed += OnUndoRedoPerformed;
|
||||
}
|
||||
|
||||
void OnUndoRedoPerformed()
|
||||
{
|
||||
UserChoicesObject.instance.Save();
|
||||
m_QuestionnaireView?.Refresh();
|
||||
UpdateRecommendation(keepSelection:true);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Undo.undoRedoPerformed -= OnUndoRedoPerformed;
|
||||
m_RecommendationView?.Clear();
|
||||
m_QuestionnaireView?.Clear();
|
||||
RootVisualElement?.Clear();
|
||||
m_QuestionnaireView = null;
|
||||
m_RecommendationView = null;
|
||||
}
|
||||
|
||||
public void SetVisible(bool visible)
|
||||
{
|
||||
RootVisualElement.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
m_IsVisible = visible;
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
Debug.Assert(MultiplayerCenterAnalytics != null, "MultiplayerCenterAnalytics != null");
|
||||
RefreshPreReleaseHandling();
|
||||
if (!m_ShouldRefresh && RootVisualElement.childCount > 0) return;
|
||||
CreateStandardView();
|
||||
m_ShouldRefresh = false;
|
||||
}
|
||||
|
||||
void RefreshPreReleaseHandling()
|
||||
{
|
||||
if (!m_PreReleaseHandling.IsReady)
|
||||
{
|
||||
m_PreReleaseHandling.OnAllChecksFinished += PatchData;
|
||||
m_PreReleaseHandling.CheckForUpdates();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_PreReleaseHandling.PatchRecommenderSystemData();
|
||||
}
|
||||
}
|
||||
|
||||
void CreateStandardView()
|
||||
{
|
||||
Clear();
|
||||
Undo.undoRedoPerformed -= OnUndoRedoPerformed;
|
||||
Undo.undoRedoPerformed += OnUndoRedoPerformed;
|
||||
MigrateUserChoices();
|
||||
|
||||
// We need this because Bottom bar is a part of the Recommendations Tab and it should always stay
|
||||
// at the bottom of the view. So we need to make sure that the root tab element is always 100% height.
|
||||
RootVisualElement.style.height = Length.Percent(100);
|
||||
|
||||
var horizontalContainer = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
|
||||
horizontalContainer.AddToClassList(StyleClasses.MainSplitView);
|
||||
horizontalContainer.name = "recommendation-tab-split-view";
|
||||
|
||||
// This is used to make sure the left side does not grow to 100% as this is what would happen by default.
|
||||
// It feels not 100% correct. But it seems to be the only way to match the height of the 2 sides with how
|
||||
// our views are build currently.
|
||||
horizontalContainer.contentContainer.style.position = Position.Relative;
|
||||
|
||||
m_QuestionnaireView = new QuestionnaireView(QuestionnaireObject.instance.Questionnaire);
|
||||
m_QuestionnaireView.OnQuestionnaireDataChanged += HandleQuestionnaireDataChanged;
|
||||
m_QuestionnaireView.OnPresetSelected += OnPresetSelected;
|
||||
m_QuestionnaireView.Root.AddToClassList(StyleClasses.MainSplitViewLeft);
|
||||
horizontalContainer.Add(m_QuestionnaireView.Root);
|
||||
|
||||
m_RecommendationView = new RecommendationView();
|
||||
m_RecommendationView.Root.AddToClassList(StyleClasses.MainSplitViewRight);
|
||||
horizontalContainer.Add(m_RecommendationView.Root);
|
||||
RootVisualElement.Add(horizontalContainer);
|
||||
|
||||
m_BottomBarView = new RecommendationViewBottomBar(MultiplayerCenterAnalytics);
|
||||
m_RecommendationView.OnPackageSelectionChanged +=
|
||||
() => m_BottomBarView.UpdatePackagesToInstall(m_RecommendationView.CurrentRecommendation, m_RecommendationView.AllPackages);
|
||||
RootVisualElement.Add(m_BottomBarView);
|
||||
UpdateRecommendation(keepSelection: true);
|
||||
|
||||
m_BottomBarView.SetInfoTextForCheckingPackages(!m_PreReleaseHandling.IsReady);
|
||||
}
|
||||
|
||||
void HandleQuestionnaireDataChanged()
|
||||
{
|
||||
UpdateRecommendation(keepSelection: false);
|
||||
}
|
||||
|
||||
static void MigrateUserChoices()
|
||||
{
|
||||
var questionnaire = QuestionnaireObject.instance.Questionnaire;
|
||||
var userChoices = UserChoicesObject.instance;
|
||||
|
||||
// make sure the version of the questionnaire is the same as the one in the user choices.
|
||||
if (questionnaire.Version != userChoices.QuestionnaireVersion && userChoices.UserAnswers.Answers.Count > 0)
|
||||
{
|
||||
Logic.MigrateUserChoices(questionnaire, userChoices);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateRecommendation(bool keepSelection)
|
||||
{
|
||||
var questionnaire = QuestionnaireObject.instance.Questionnaire;
|
||||
var userChoices = UserChoicesObject.instance;
|
||||
|
||||
var errors = Logic.ValidateAnswers(questionnaire, userChoices.UserAnswers);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
Debug.LogError(error);
|
||||
}
|
||||
|
||||
var recommendation = userChoices.Preset == Preset.None? null
|
||||
: RecommenderSystem.GetRecommendation(questionnaire, userChoices.UserAnswers);
|
||||
m_PreReleaseHandling.PatchPackages(recommendation);
|
||||
if (keepSelection)
|
||||
{
|
||||
RecommendationUtils.ApplyPreviousSelection(recommendation, userChoices.SelectedSolutions);
|
||||
}
|
||||
else if (recommendation != null) // we only send the event if there is a recommendation and it is a new one
|
||||
{
|
||||
MultiplayerCenterAnalytics.SendRecommendationEvent(userChoices.UserAnswers, userChoices.Preset);
|
||||
}
|
||||
|
||||
m_RecommendationView.UpdateRecommendation(recommendation, m_PreReleaseHandling);
|
||||
m_BottomBarView.UpdatePackagesToInstall(recommendation, m_RecommendationView.AllPackages);
|
||||
}
|
||||
|
||||
void PatchData()
|
||||
{
|
||||
m_PreReleaseHandling.PatchRecommenderSystemData();
|
||||
m_PreReleaseHandling.OnAllChecksFinished -= PatchData;
|
||||
m_ShouldRefresh = true;
|
||||
if(m_IsVisible)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void OnPresetSelected(Preset preset)
|
||||
{
|
||||
var (resultAnswerData, recommendation) = Logic.ApplyPresetToAnswerData(
|
||||
UserChoicesObject.instance.UserAnswers, preset, QuestionnaireObject.instance.Questionnaire);
|
||||
|
||||
UserChoicesObject.instance.UserAnswers = resultAnswerData;
|
||||
UserChoicesObject.instance.Save();
|
||||
|
||||
if (recommendation != null)
|
||||
MultiplayerCenterAnalytics.SendRecommendationEvent(resultAnswerData, preset);
|
||||
|
||||
m_QuestionnaireView.Refresh();
|
||||
m_RecommendationView.UpdateRecommendation(recommendation, m_PreReleaseHandling);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 052c764978664486a9c04518899ddf57
|
||||
timeCreated: 1700489744
|
@@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Multiplayer.Center.Analytics;
|
||||
using Unity.Multiplayer.Center.Questionnaire;
|
||||
using Unity.Multiplayer.Center.Recommendations;
|
||||
using Unity.Multiplayer.Center.Window.UI;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Window
|
||||
{
|
||||
class RecommendationViewBottomBar : VisualElement
|
||||
{
|
||||
readonly Label m_PackageCount;
|
||||
readonly Button m_InstallPackageButton;
|
||||
readonly Label m_InfoLabel;
|
||||
|
||||
IMultiplayerCenterAnalytics m_Analytics;
|
||||
|
||||
MultiplayerCenterWindow m_Window = EditorWindow.GetWindow<MultiplayerCenterWindow>();
|
||||
List<string> m_PackagesToInstallIds = new ();
|
||||
List<string> m_PackagesToInstallNames = new ();
|
||||
RecommendationViewData m_RecommendationViewData;
|
||||
SolutionsToRecommendedPackageViewData m_SolutionToPackageData;
|
||||
|
||||
public RecommendationViewBottomBar(IMultiplayerCenterAnalytics analytics)
|
||||
{
|
||||
m_Analytics = analytics;
|
||||
name = "bottom-bar";
|
||||
m_PackageCount = new Label {name = "package-count"};
|
||||
m_InfoLabel = new Label();
|
||||
|
||||
// Setup Install Button
|
||||
m_InstallPackageButton = new Button(OnInstallButtonClicked) {text = "Install Packages"};
|
||||
m_InstallPackageButton.AddToClassList(StyleClasses.NextStepButton);
|
||||
|
||||
// Put the button in a container
|
||||
var installPackageContainer = new VisualElement() {name = "install-package-container"};
|
||||
installPackageContainer.Add(m_InstallPackageButton);
|
||||
|
||||
Add(m_PackageCount);
|
||||
Add(m_InfoLabel);
|
||||
Add(installPackageContainer);
|
||||
}
|
||||
|
||||
void OnInstallButtonClicked()
|
||||
{
|
||||
if (!PackageManagement.IsAnyMultiplayerPackageInstalled() || WarnDialogForPackageInstallation())
|
||||
{
|
||||
SendInstallationAnalyticsEvent();
|
||||
InstallSelectedPackagesAndExtension();
|
||||
}
|
||||
}
|
||||
|
||||
void SendInstallationAnalyticsEvent()
|
||||
{
|
||||
var answerObject = UserChoicesObject.instance;
|
||||
m_Analytics.SendInstallationEvent(answerObject.UserAnswers, answerObject.Preset,
|
||||
AnalyticsUtils.GetPackagesWithAnalyticsFormat(m_RecommendationViewData, m_SolutionToPackageData));
|
||||
}
|
||||
|
||||
bool WarnDialogForPackageInstallation()
|
||||
{
|
||||
var warningMessage =
|
||||
"Ensure compatibility with your current multiplayer packages before installing or upgrading the following:\n" +
|
||||
string.Join("\n", m_PackagesToInstallNames);
|
||||
return EditorUtility.DisplayDialog("Install Packages", warningMessage, "OK", "Cancel");
|
||||
}
|
||||
|
||||
void InstallSelectedPackagesAndExtension()
|
||||
{
|
||||
SetInfoTextForInstallation(isInstalling:true);
|
||||
m_Window.DisableUiForInstallation();
|
||||
PackageManagement.InstallPackages(m_PackagesToInstallIds, onAllInstalled: OnInstallationFinished);
|
||||
}
|
||||
|
||||
void OnInstallationFinished(bool success)
|
||||
{
|
||||
SetInfoTextForInstallation(isInstalling:false);
|
||||
m_Window.RequestShowGettingStartedTabAfterDomainReload();
|
||||
m_Window.ReenableUiAfterInstallation();
|
||||
}
|
||||
|
||||
public void UpdatePackagesToInstall(RecommendationViewData data, SolutionsToRecommendedPackageViewData packageViewData)
|
||||
{
|
||||
m_RecommendationViewData = data;
|
||||
m_SolutionToPackageData = packageViewData;
|
||||
var packages = RecommendationUtils.PackagesToInstall(data, packageViewData);
|
||||
RecommendationUtils.GetPackagesWithAdditionalPackages(packages, out m_PackagesToInstallIds, out m_PackagesToInstallNames, out var toolTip);
|
||||
m_PackageCount.tooltip = toolTip;
|
||||
|
||||
// Note: quickstart is counted in the list of packages to install, but not the names
|
||||
m_PackageCount.text = $"Packages to install: {m_PackagesToInstallNames.Count}";
|
||||
// if the list is empty, disable the button
|
||||
m_InstallPackageButton.SetEnabled(m_PackagesToInstallNames.Count > 0);
|
||||
}
|
||||
|
||||
internal void SetInfoTextForInstallation(bool isInstalling)
|
||||
{
|
||||
SetInfoLabelTextAndVisibility("Downloading packages, please wait ...", isInstalling);
|
||||
}
|
||||
|
||||
internal void SetInfoTextForCheckingPackages(bool isChecking)
|
||||
{
|
||||
SetInfoLabelTextAndVisibility("Querying packages information ...", isChecking);
|
||||
|
||||
// Handle the case of reopening the window during the installation.
|
||||
// When reopening the window, the packages are being checked. Once that check is done, we still want to
|
||||
// display the installation package text if there is an ongoing installation.
|
||||
if(!isChecking && !PackageManagement.IsInstallationFinished())
|
||||
SetInfoTextForInstallation(isInstalling:true);
|
||||
}
|
||||
|
||||
void SetInfoLabelTextAndVisibility(string text, bool isVisible)
|
||||
{
|
||||
m_InfoLabel.text = text;
|
||||
m_InfoLabel.visible = isVisible;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1084186a4004823abee9b87c8ddf198
|
||||
timeCreated: 1710930997
|
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
using Unity.Multiplayer.Center.Analytics;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Window
|
||||
{
|
||||
// Note: there is a TabView API in UI Toolkit, but only starting from 2023.2
|
||||
internal interface ITabView
|
||||
{
|
||||
/// <summary>
|
||||
/// The name as displayed in the tab button
|
||||
/// Should be serialized.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The root visual element of the tab view.
|
||||
/// The setter will only be used if the root visual element is null when the tab is created.
|
||||
/// </summary>
|
||||
VisualElement RootVisualElement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the tab view visible or not.
|
||||
/// </summary>
|
||||
/// <param name="visible">If true, visible.</param>
|
||||
void SetVisible(bool visible);
|
||||
|
||||
/// <summary>
|
||||
/// If true the Tab can be selected by the user.
|
||||
/// </summary>
|
||||
bool IsEnabled => true;
|
||||
|
||||
/// <summary>
|
||||
/// Tooltip which will be shown on the Tab Button.
|
||||
/// </summary>
|
||||
string ToolTip => "";
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the UI Elements according to latest data.
|
||||
/// If the UI is not created yet, it does it.
|
||||
/// </summary>
|
||||
void Refresh();
|
||||
|
||||
/// <summary>
|
||||
/// Unregister all events and clear UI Elements
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// The Multiplayer Center Analytics provider.
|
||||
/// </summary>
|
||||
IMultiplayerCenterAnalytics MultiplayerCenterAnalytics { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
internal class TabGroup
|
||||
{
|
||||
const string k_TabViewName = "tab-view";
|
||||
const string k_TabZoneName = "tab-zone";
|
||||
const string k_TabButtonUssClass = "tab-button";
|
||||
|
||||
// The container for all the tabs
|
||||
const string k_TabsContainerUssClass ="tabs-container";
|
||||
|
||||
// Gets applied to the root of each tab
|
||||
const string k_TabContentUssClass = "tab-content";
|
||||
|
||||
[field: SerializeField]
|
||||
public int CurrentTab { get; private set; } = -1;
|
||||
|
||||
public int ViewCount => m_TabViews?.Length ?? 0;
|
||||
|
||||
VisualElement[] m_TabButtons;
|
||||
|
||||
[SerializeReference]
|
||||
ITabView[] m_TabViews;
|
||||
|
||||
public VisualElement Root { get; private set; }
|
||||
|
||||
VisualElement m_MainContainer;
|
||||
|
||||
IMultiplayerCenterAnalytics m_MultiplayerCenterAnalytics;
|
||||
|
||||
internal IMultiplayerCenterAnalytics MultiplayerCenterAnalytics
|
||||
{
|
||||
get => m_MultiplayerCenterAnalytics;
|
||||
set
|
||||
{
|
||||
m_MultiplayerCenterAnalytics = value;
|
||||
foreach (var tabView in m_TabViews)
|
||||
{
|
||||
if(tabView != null)
|
||||
tabView.MultiplayerCenterAnalytics = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TabGroup(IMultiplayerCenterAnalytics analytics, ITabView[] tabViews, int defaultIndex = 0)
|
||||
{
|
||||
m_TabViews = tabViews;
|
||||
CurrentTab = defaultIndex;
|
||||
MultiplayerCenterAnalytics = analytics;
|
||||
}
|
||||
|
||||
public void SetSelected(int index, bool force = false)
|
||||
{
|
||||
// Select the first tab, if the requested tab is not enabled.
|
||||
// This assumes the first tab is always enabled.
|
||||
if (!m_TabViews[index].IsEnabled)
|
||||
index = 0;
|
||||
|
||||
if (index == CurrentTab && !force)
|
||||
return;
|
||||
|
||||
if (CurrentTab >= 0 && CurrentTab < m_TabViews.Length)
|
||||
{
|
||||
m_TabButtons[CurrentTab].RemoveFromClassList("selected");
|
||||
m_TabViews[CurrentTab].SetVisible(false);
|
||||
}
|
||||
|
||||
EditorPrefs.SetInt(PlayerSettings.productName + "_MultiplayerCenter_TabIndex", index);
|
||||
CurrentTab = index;
|
||||
m_TabViews[CurrentTab].Refresh();
|
||||
m_TabButtons[CurrentTab].AddToClassList("selected");
|
||||
m_TabViews[CurrentTab].SetVisible(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates the visual elements for all the tabs.
|
||||
/// Use this to create the tabs for the first time the UI is shown or after a domain reload.
|
||||
/// </summary>
|
||||
public void CreateTabs()
|
||||
{
|
||||
Root ??= new VisualElement();
|
||||
m_MainContainer ??= new VisualElement();
|
||||
|
||||
if (Root.Q(k_TabZoneName) != null)
|
||||
Root.Q(k_TabZoneName).RemoveFromHierarchy();
|
||||
|
||||
var tabZone = new VisualElement() {name = k_TabZoneName};
|
||||
Root.Add(tabZone);
|
||||
Root.name = k_TabViewName;
|
||||
m_TabButtons = new VisualElement[m_TabViews.Length];
|
||||
for (var i = 0; i < m_TabViews.Length; i++)
|
||||
{
|
||||
var tabView = m_TabViews[i];
|
||||
var index = i; // copy for closure
|
||||
var tabButton = new Button(() => SetSelected(index));
|
||||
tabButton.enabledSelf = tabView.IsEnabled;
|
||||
tabButton.tooltip = tabView.ToolTip;
|
||||
tabButton.AddToClassList(k_TabButtonUssClass);
|
||||
tabButton.text = tabView.Name;
|
||||
tabZone.Add(tabButton);
|
||||
m_TabButtons[i] = tabButton;
|
||||
tabView.RootVisualElement ??= new VisualElement();
|
||||
tabView.RootVisualElement.AddToClassList(k_TabContentUssClass);
|
||||
tabView.RootVisualElement.style.display = DisplayStyle.None;
|
||||
m_MainContainer.Add(m_TabViews[i].RootVisualElement);
|
||||
}
|
||||
|
||||
m_MainContainer.AddToClassList(k_TabsContainerUssClass);
|
||||
Root.Add(m_MainContainer);
|
||||
CurrentTab = EditorPrefs.GetInt(PlayerSettings.productName + "_MultiplayerCenter_TabIndex", 0);
|
||||
}
|
||||
|
||||
static void SetVisible(VisualElement e, bool visible)
|
||||
{
|
||||
e.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
if(m_TabViews == null)
|
||||
return;
|
||||
|
||||
foreach (var tabView in m_TabViews)
|
||||
{
|
||||
tabView?.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TabsAreValid()
|
||||
{
|
||||
if (m_TabViews == null)
|
||||
return false;
|
||||
|
||||
foreach (var tab in m_TabViews)
|
||||
{
|
||||
if (tab == null)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fdefed6b790f4373b0feb7d25854d8ac
|
||||
timeCreated: 1700582398
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bd34ec3fe8f4aed936c3a0cf2f32e56
|
||||
timeCreated: 1695037065
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user