This commit is contained in:
2025-01-17 13:10:42 +01:00
commit 4536213c91
15115 changed files with 1442174 additions and 0 deletions

View File

@@ -0,0 +1,340 @@
using System;
using System.Collections.Generic;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Recommendations;
using UnityEngine;
using static System.Int32;
namespace Unity.Multiplayer.Center.Questionnaire
{
using AnswerMap = System.Collections.Generic.Dictionary<string, List<string>>;
/// <summary>
/// Question and answer logic manipulations.
/// </summary>
static internal class Logic
{
/// <summary>
/// Checks if the answers make sense and correspond to the questions.
/// </summary>
/// <param name="questions">The reference questionnaire</param>
/// <param name="currentAnswers">The validated answers.</param>
/// <returns>A list of problems or an empty list.</returns>
public static List<string> ValidateAnswers(QuestionnaireData questions, AnswerData currentAnswers)
{
if (questions == null || questions.Questions == null || questions.Questions.Length < 1)
return new List<string> {"No questions found in questionnaire"};
// TODO: check all question Id exist and are different
// TODO: check the questions have at least two possible answers
var errors = new List<string>();
for (int i = 0; i < currentAnswers.Answers.Count; ++i)
{
var current = currentAnswers.Answers[i];
if (string.IsNullOrEmpty(current.QuestionId))
errors.Add($"AnswerData at index {i}: Question id is empty");
if (!TryGetQuestionByQuestionId(questions, current.QuestionId, out var question))
errors.Add($"AnswerData at index {i}: Question id {current.QuestionId} not found in questionnaire");
if (current.Answers == null || current.Answers.Count < 1) // TODO: in the future we might have questions with nothing selected being a valid answer
{
errors.Add($"AnswerData at index {i}: No answers found (question {current.QuestionId})");
}
else if (question != null)
{
switch (question.ViewType)
{
case ViewType.Toggle when current.Answers.Count > 1:
case ViewType.Radio when current.Answers.Count > 1:
case ViewType.DropDown when current.Answers.Count != 1:
errors.Add($"AnswerData at index {i}: Too many answers (question {current.QuestionId})");
break;
}
}
}
return errors;
}
/// <summary>
/// Finds the first unanswered question index, aka the first question to display.
/// If all questions have been answered, the index is the length of the questions array.
/// </summary>
/// <param name="questions">The questionnaire</param>
/// <param name="currentAnswers">the answers so far</param>
/// <returns>The index in the questions array or the size of the questions array</returns>
public static int FindFirstUnansweredQuestion(QuestionnaireData questions, AnswerData currentAnswers)
{
if (currentAnswers.Answers.Count < 1)
return 0;
var dico = AnswersToDictionary(questions, currentAnswers); // Assumes validation has already taken place.
for (int i = 0; i < questions.Questions.Length; i++)
{
var current = questions.Questions[i];
bool isAnswered = dico.ContainsKey(current.Id); // Assumes validation has already taken place.
if (!isAnswered)
return i;
}
// all answered
return questions.Questions.Length;
}
/// <summary>
/// Conversion to a dictionary for easier manipulation.
/// </summary>
/// <param name="questions">the questionnaire</param>
/// <param name="currentAnswers">the answers so far</param>
/// <returns>The dictionary containing (id, list of answer id) </returns>
internal static AnswerMap AnswersToDictionary(QuestionnaireData questions, AnswerData currentAnswers)
{
var dico = new AnswerMap();
foreach (var answer in currentAnswers.Answers)
{
dico.Add(answer.QuestionId, answer.Answers);
}
return dico;
}
/// <summary>
/// Checks if the question id exists in the questionnaire and return it.
/// </summary>
/// <param name="questions">The questionnaire</param>
/// <param name="questionId">The id to find</param>
/// <param name="foundQuestion">The found question or null</param>
/// <returns>True if found, else false</returns>
internal static bool TryGetQuestionByQuestionId(QuestionnaireData questions, string questionId, out Question foundQuestion)
{
foreach (var q in questions.Questions)
{
if (q.Id == questionId)
{
foundQuestion = q;
return true;
}
}
foundQuestion = null;
return false;
}
internal static bool TryGetAnswerByQuestionId(AnswerData answers, string questionId, out AnsweredQuestion foundAnswer)
{
return TryGetAnswerByQuestionId(answers.Answers, questionId, out foundAnswer);
}
internal static bool TryGetAnswerByQuestionId(IEnumerable<AnsweredQuestion> answerList, string questionId, out AnsweredQuestion foundAnswer)
{
foreach (var aq in answerList)
{
if (aq.QuestionId == questionId)
{
foundAnswer = aq;
return true;
}
}
foundAnswer = null;
return false;
}
internal static bool TryGetAnswerByAnswerId(Question question, string answerId, out Answer answer)
{
answer = null;
foreach (var choice in question.Choices)
{
if (answerId == choice.Id)
{
answer = choice;
return true;
}
}
return false;
}
/// <summary>
/// Applies the preset to the current answers and returns the new answers and the recommendation.
/// It skips all the mandatory questions, which have to be answered separately.
/// </summary>
/// <param name="currentAnswers">The answers the user selected so far</param>
/// <param name="preset">The preset to apply</param>
/// <param name="questionnaire">All the available questions</param>
/// <returns>The new answer data and the associated recommendation(might be null).</returns>
public static (AnswerData, RecommendationViewData) ApplyPresetToAnswerData(AnswerData currentAnswers, Preset preset,
QuestionnaireData questionnaire)
{
if (preset == Preset.None) return (new AnswerData(), null);
var presetData = questionnaire.PresetData;
var index = Array.IndexOf(presetData.Presets, preset);
var presetAnswers = presetData.Answers[index];
var resultAnswerData = presetAnswers.Clone();
foreach (var question in questionnaire.Questions)
{
if (!question.IsMandatory) continue;
if (TryGetAnswerByQuestionId(currentAnswers, question.Id, out var currentAnswer))
Update(resultAnswerData, currentAnswer);
}
var recommendation = RecommenderSystem.GetRecommendation(questionnaire, resultAnswerData);
return (resultAnswerData, recommendation);
}
public static void Update(AnswerData data, AnsweredQuestion a)
{
if (a.Answers == null || a.Answers.Count < 1)
{
// TODO: this might need to change in the future
return;
}
if (data.Answers == null)
{
data.Answers = new List<AnsweredQuestion>() {a};
return;
}
for (int i = 0; i < data.Answers.Count; i++)
{
if (data.Answers[i].QuestionId == a.QuestionId)
{
data.Answers[i] = a;
return;
}
}
data.Answers.Add(a);
}
public static bool AreMandatoryQuestionsFilled(QuestionnaireData questionnaire, AnswerData answers)
{
var mandatoryQuestions = new List<string>();
foreach (var question in questionnaire.Questions)
{
if (question.IsMandatory)
{
mandatoryQuestions.Add(question.Id);
}
}
var foundAnswers = new bool[mandatoryQuestions.Count];
foreach (var answer in answers.Answers)
{
for (var i = 0; i < mandatoryQuestions.Count; i++)
{
if (answer.QuestionId == mandatoryQuestions[i] && answer.Answers.Count > 0)
{
foundAnswers[i] = true;
break;
}
}
}
foreach (var answer in foundAnswers)
{
if (!answer)
{
return false;
}
}
return true;
}
public static SelectedSolutionsData.HostingModel ConvertInfrastructure(RecommendedSolutionViewData serverArchitectureOption)
{
return serverArchitectureOption.Solution switch
{
PossibleSolution.LS => SelectedSolutionsData.HostingModel.ClientHosted,
PossibleSolution.DS => SelectedSolutionsData.HostingModel.DedicatedServer,
PossibleSolution.CloudCode => SelectedSolutionsData.HostingModel.CloudCode,
PossibleSolution.DA => SelectedSolutionsData.HostingModel.DistributedAuthority,
_ => SelectedSolutionsData.HostingModel.None
};
}
public static SelectedSolutionsData.NetcodeSolution ConvertNetcodeSolution(RecommendedSolutionViewData netcodeOption)
{
return netcodeOption.Solution switch
{
PossibleSolution.NGO => SelectedSolutionsData.NetcodeSolution.NGO,
PossibleSolution.N4E => SelectedSolutionsData.NetcodeSolution.N4E,
PossibleSolution.CustomNetcode => SelectedSolutionsData.NetcodeSolution.CustomNetcode,
PossibleSolution.NoNetcode => SelectedSolutionsData.NetcodeSolution.NoNetcode,
_ => SelectedSolutionsData.NetcodeSolution.None
};
}
public static void MigrateUserChoices(QuestionnaireData questionnaire, UserChoicesObject userChoices)
{
var versionBeforeMigration = string.IsNullOrEmpty(userChoices.QuestionnaireVersion) ? "1.0" : userChoices.QuestionnaireVersion;
// first migration because field was not present
if (questionnaire.Version is "1.2" && IsVersionLower(versionBeforeMigration, "1.2"))
{
if (TryGetAnswerByQuestionId(userChoices.UserAnswers, "Competitiveness", out var competitiveQuestion))
{
userChoices.UserAnswers.Answers.Remove(competitiveQuestion);
}
// this will write the current version.
userChoices.Save();
}
// Medium Pace Option was removed fall back to slow.
if (questionnaire.Version is "1.3" && IsVersionLower(versionBeforeMigration, "1.3"))
{
if (userChoices.UserAnswers.Answers != null && TryGetAnswerByQuestionId(userChoices.UserAnswers, "Pace", out var paceQuestion))
{
if (paceQuestion.Answers.Contains("Medium"))
{
// Set the answer to slow, as in the sheet, we changed all medium to slow.
// So this is probably the best guess.
paceQuestion.Answers.Remove("Medium");
paceQuestion.Answers.Add("Slow");
}
}
// this will write the current version.
userChoices.Save();
}
}
/// <summary>
/// Compares two versions and returns true if the versionToTest is lower than the currentVersion.
/// </summary>
/// <param name="versionToTest">The version number that gets tested</param>
/// <param name="currentVersion">The version number to test against</param>
/// <returns>True if versionToTest is lower than currentVersion</returns>
internal static bool IsVersionLower(string versionToTest, string currentVersion)
{
var versionToTestParts = versionToTest.Split('.');
var currentVersionParts = currentVersion.Split('.');
for (var i = 0; i < Math.Min(versionToTestParts.Length, currentVersionParts.Length); i++)
{
var canParseCurrentVersion = TryParse(currentVersionParts[i], out var currentVersionPart);
var canParseVersionToTestVersion = TryParse(versionToTestParts[i], out var versionToTestPart);
if (canParseCurrentVersion == false || canParseVersionToTestVersion == false)
{
Debug.LogError("Version number is not in the correct format");
return false;
}
if ( versionToTestPart != currentVersionPart)
return versionToTestPart < currentVersionPart;
}
return versionToTestParts.Length < currentVersionParts.Length;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d9beb5106d424f23902054f76d4b673d
timeCreated: 1695634195

View File

@@ -0,0 +1,25 @@
using System;
using Unity.Multiplayer.Center.Common;
using UnityEngine;
namespace Unity.Multiplayer.Center.Questionnaire
{
/// <summary>
/// The predefined answers for presets. This is meant to be read only and saved with the questionnaire
/// </summary>
[Serializable]
internal class PresetData
{
/// <summary>
/// The list of presets for which we have predefined answers (hopefully all possible values except None)
/// This should contain as many values as the <see cref="Answers"/> array.
/// </summary>
public Preset[] Presets;
/// <summary>
/// The predefined answers for each preset, in the same order as <see cref="Presets"/>.
/// This should contain as many values as the <see cref="Presets"/> array.
/// </summary>
public AnswerData[] Answers;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3595bcaa1af84e6d92e07f0258a4256c
timeCreated: 1699627067

View File

@@ -0,0 +1,417 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &1
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c3677cd9ede84e78b2fff6331c6301da, type: 3}
m_Name: Questionnaire
m_EditorClassIdentifier: Unity.Multiplayer.Center.Questionnaire.QuestionnaireObject
Questionnaire:
FormatVersion: 1.0.0
Version: 1.3
Questions:
- Id: PlayerCount
Title: Number of Players per Session
Description: The number of expected players in a session influences the Netcode
stack and the hosting model.
GlobalWeight: 1
ViewType: 3
Choices:
- Id: 2
Title: 2
Description:
ScoreImpacts:
- Solution: 0
Score: 0.7
Comment: can be used for 2 player games
- Solution: 2
Score: 0.8
Comment: is possible with 2 players
- Solution: 4
Score: 0.2
Comment: can handle 2 players
- Id: 4
Title: 4
Description:
ScoreImpacts:
- Solution: 0
Score: 0.7
Comment: can be used for 4 player games
- Solution: 2
Score: 0.7
Comment: is possible with 4 players
- Solution: 4
Score: 0.2
Comment: can handle 4 players
- Id: 8
Title: 8
Description:
ScoreImpacts:
- Solution: 0
Score: 0.7
Comment: can be used for 8 player games
- Solution: 4
Score: 0.2
Comment: can handle 8 players
- Solution: 2
Score: 0.4
Comment: is possible with 8 players
- Id: 16
Title: 16
Description:
ScoreImpacts:
- Solution: 1
Score: 0.5
Comment: can be used for 16 player games
- Solution: 3
Score: 0.5
Comment: is necessary with a high number of players
- Solution: 4
Score: 0.3
Comment: can handle 16 players
- Id: 64+
Title: 64
Description:
ScoreImpacts:
- Solution: 1
Score: 0.5
Comment: can be used for 64 player games
- Solution: 3
Score: 1
Comment: is necessary with a high number of players
- Solution: 4
Score: 0.3
Comment: can handle 64 players
- Id: 128
Title: 128
Description:
ScoreImpacts:
- Solution: 1
Score: 10
Comment: should be used for a very high number of players
- Solution: 3
Score: 1000
Comment: is necessary with a high number of players
- Id: 256
Title: 256
Description:
ScoreImpacts:
- Solution: 1
Score: 10
Comment: should be used for a very high number of players
- Solution: 3
Score: 1000
Comment: is necessary with a high number of players
- Id: 512
Title: 512+
Description:
ScoreImpacts:
- Solution: 1
Score: 10
Comment: should be used for a very high number of players
- Solution: 3
Score: 1000
Comment: is necessary with a high number of players
IsMandatory: 1
- Id: Pace
Title: Gameplay Pace
Description: Gameplay pace indicates how sensitive gameplay is to latency.
The amount of latency you expect impacts Netcode stacks and hosting models.
GlobalWeight: 1
ViewType: 0
Choices:
- Id: Slow
Title: Slow
Description:
ScoreImpacts:
- Solution: 0
Score: 0.7
Comment: can be used for slow-paced games
- Solution: 2
Score: 0.25
Comment: is sufficient for Slow Games
- Solution: 4
Score: 0.25
Comment: works well with slow games
- Id: Fast
Title: Fast
Description:
ScoreImpacts:
- Solution: 1
Score: 0.75
Comment: tends to be better for fast paced games
- Solution: 3
Score: 1
Comment: is recommended for fast games
- Solution: 4
Score: 0.65
Comment: can handle fast gameplay
IsMandatory: 0
- Id: Cheating
Title: Cheating / Modding Prevention
Description: The most common anti-cheating approach is to hide a part of your
gameplay implementation from clients using a server-authoritative hosting
model
GlobalWeight: 1
ViewType: 0
Choices:
- Id: CheatingNotImportant
Title: Not so important
Description:
ScoreImpacts:
- Solution: 2
Score: 0.2
Comment: is more cost-effective if cheating is not very important
- Solution: 0
Score: 0.6
Comment: is preferred if cheating prevention is not so important
- Id: CheatingImportant
Title: Very important
Description:
ScoreImpacts:
- Solution: 3
Score: 1000
Comment: is required if cheating prevention is important
- Solution: 1
Score: 0.5
Comment: is preferred if cheating prevention is important
IsMandatory: 0
- Id: CostSensitivity
Title: Cost Sensitivity
Description: Depending on your player monetization, your sensitivity to costs
will vary and influence the recommended hosting model.
GlobalWeight: 1
ViewType: 0
Choices:
- Id: BestExperience
Title: Favor best player experience
Description: When the game monetization heavily depends on the best possible player experience, often for e-sport competitive titles, backend solutions tend to be more expensive.
ScoreImpacts:
- Solution: 3
Score: 1.5
Comment: offers great performance
- Solution: 4
Score: 1.2
Comment: offers good performance
- Solution: 1
Score: 0.85
Comment: offers good performance
- Id: BestMargin
Title: Favor best operating costs
Description: At the expense of some degradation of the player experience, smart relay solutions can provide a cost-effective solution before adopting dedicated cloud-hosted servers.
ScoreImpacts:
- Solution: 4
Score: 1
Comment: is cost-effective
- Solution: 0
Score: 0.5
Comment: is a good compromise if you want to optimize your operating costs
- Id: NoCost
Title: As little cost as possible
Description: Avoiding backend costs usually involves implementing a client-hosted solution.
ScoreImpacts:
- Solution: 3
Score: -100
Comment: costs money
- Solution: 4
Score: 1
Comment: is cost-effective
- Solution: 1
Score: 0.5
Comment: is performant and enables you to reduce compute costs
IsMandatory: 0
- Id: NetcodeArchitecture
Title: Netcode Architecture
Description: Specific game genres often require very specialized netcode architectures.
In that case, the netcode stack recommendation might suggest a custom or third-party netcode that can be implemented on top of a Unity netcode, or with
the help of a third-party solution.
GlobalWeight: 1
ViewType: 0
Choices:
- Id: ClientServer
Title: Client / Server
Description: The most common netcode architecture is client-server. This architecture can be used with a client-hosted or cloud-hosted model.
ScoreImpacts: []
- Id: LockstepSimulation
Title: Deterministic Lockstep
Description: Synchronization method in multiplayer games where each player's computer processes the same game inputs in the same order to maintain consistency in the game state.
ScoreImpacts:
- Solution: 5
Score: 5000
Comment: is necessary for Lockstep simulation
- Solution: 2
Score: 5000
Comment: is necessary for Lockstep simulation
- Id: MultiServerSessions
Title: Multi-Server Sessions
Description: Massively multiplayer online titles often require complex and tailored multi-server netcode and backend solutions.
ScoreImpacts:
- Solution: 5
Score: 100
Comment: is required for Multi-Server Sessions
- Solution: 3
Score: 1000
Comment: is required for Multi-Server Sessions
- Id: NoNetcode
Title: No netcode
Description: Netcode frameworks are usually meant to support interactive multiplayer gameplay. If the multiplayer experience relies on asynchronous social features or waits for someone's turn, there is often no netcode involved.
ScoreImpacts:
- Solution: 6
Score: 1000
Comment: is necessary
- Solution: 7
Score: 3000
Comment: enables you to run your game logic in the cloud as serverless functions
IsMandatory: 0
PresetData:
Presets: 0200000005000000030000000600000007000000010000000800000004000000090000000a0000000b000000
Answers:
- Answers:
- QuestionId: Pace
Answers:
- Fast
- QuestionId: Cheating
Answers:
- CheatingImportant
- QuestionId: CostSensitivity
Answers:
- BestExperience
- QuestionId: NetcodeArchitecture
Answers:
- ClientServer
- Answers:
- QuestionId: Pace
Answers:
- Slow
- QuestionId: Cheating
Answers:
- CheatingNotImportant
- QuestionId: CostSensitivity
Answers:
- NoCost
- QuestionId: NetcodeArchitecture
Answers:
- ClientServer
- Answers:
- QuestionId: Pace
Answers:
- Fast
- QuestionId: Cheating
Answers:
- CheatingImportant
- QuestionId: CostSensitivity
Answers:
- BestExperience
- QuestionId: NetcodeArchitecture
Answers:
- ClientServer
- Answers:
- QuestionId: Pace
Answers:
- Slow
- QuestionId: Cheating
Answers:
- CheatingImportant
- QuestionId: CostSensitivity
Answers:
- BestMargin
- QuestionId: NetcodeArchitecture
Answers:
- LockstepSimulation
- Answers:
- QuestionId: Pace
Answers:
- Fast
- QuestionId: Cheating
Answers:
- CheatingImportant
- QuestionId: CostSensitivity
Answers:
- BestExperience
- QuestionId: NetcodeArchitecture
Answers:
- ClientServer
- Answers:
- QuestionId: Pace
Answers:
- Slow
- QuestionId: Cheating
Answers:
- CheatingImportant
- QuestionId: CostSensitivity
Answers:
- BestMargin
- QuestionId: NetcodeArchitecture
Answers:
- ClientServer
- Answers:
- QuestionId: Pace
Answers:
- Slow
- QuestionId: Cheating
Answers:
- CheatingImportant
- QuestionId: CostSensitivity
Answers:
- BestMargin
- QuestionId: NetcodeArchitecture
Answers:
- MultiServerSessions
- Answers:
- QuestionId: Pace
Answers:
- Slow
- QuestionId: Cheating
Answers:
- CheatingNotImportant
- QuestionId: CostSensitivity
Answers:
- BestMargin
- QuestionId: NetcodeArchitecture
Answers:
- NoNetcode
- Answers:
- QuestionId: Pace
Answers:
- Slow
- QuestionId: Cheating
Answers:
- CheatingImportant
- QuestionId: CostSensitivity
Answers:
- BestMargin
- QuestionId: NetcodeArchitecture
Answers:
- NoNetcode
- Answers:
- QuestionId: Pace
Answers:
- Fast
- QuestionId: Cheating
Answers:
- CheatingNotImportant
- QuestionId: CostSensitivity
Answers:
- NoCost
- QuestionId: NetcodeArchitecture
Answers:
- LockstepSimulation
- Answers:
- QuestionId: Pace
Answers:
- Slow
- QuestionId: Cheating
Answers:
- CheatingNotImportant
- QuestionId: CostSensitivity
Answers:
- BestMargin
- QuestionId: NetcodeArchitecture
Answers:
- ClientServer

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a659150180ae3489ba41c71780ba3779
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
using System;
using UnityEngine;
namespace Unity.Multiplayer.Center.Questionnaire
{
/// <summary>
/// The serializable data of the questionnaire
/// </summary>
[Serializable]
internal class QuestionnaireData
{
/// <summary> The version of the format to serialize/deserialize data </summary>
public string FormatVersion = "1.0.0";
/// <summary> The version of the questionnaire itself (different questions, answer choice) </summary>
public string Version ="1.2";
/// <summary> All the questions in the right order (some might be hidden though) </summary>
public Question[] Questions;
/// <summary> The predefined answers for presets. The content should match the questions.</summary>
public PresetData PresetData;
}
/// <summary>
/// Possible multiplayer solution that needs to be scored in order to assess a match. Some are mutually exclusive,
/// some are not.
/// </summary>
[Serializable]
internal enum PossibleSolution
{
/// <summary> Netcode for GameObject, incompatible with N4E </summary>
NGO,
/// <summary> Netcode for Entities, incompatible with NGO </summary>
N4E,
/// <summary> Client Hosted Architecture (also called "Listen server"; using a host and not a dedicated server) </summary>
LS,
/// <summary> Dedicated server architecture (using a dedicated server and not a host) </summary>
DS,
/// <summary> Distributed authority (Authority will be distributed across different players)</summary>
DA,
/// <summary> Not using Netcode for GameObjects nor Netcode for Entities </summary>
CustomNetcode,
/// <summary> Works asynchronously, with a database </summary>
NoNetcode,
/// <summary> Recommended backend for async games, without a Netcode (goes with <see cref="NoNetcode"/>) </summary>
CloudCode
}
[Serializable]
internal enum ViewType
{
/// <summary> Yes or No type of question best represented by a toggle</summary>
Toggle,
/// <summary> A question with multiple choices and where you can select only one answer</summary>
Radio,
/// <summary> A question with multiple choices and where you can select multiple answers</summary>
Checkboxes,
/// <summary> A question with a Drop Down</summary>
DropDown
}
[Serializable]
internal class Question
{
/// <summary> Id (unique across questions) </summary>
public string Id;
/// <summary> Short string to refer to the question (e.g. "Player Count") </summary>
public string Title;
/// <summary> Longer string to describe the question, which will be displayed in the tooltip </summary>
public string Description;
/// <summary> Optional weight to increase/decrease importance of this question, applied to all answers.</summary>
//TODO: use ignore if default
public float GlobalWeight = 1f;
/// <summary> The type of view to use to display the question </summary>
public ViewType ViewType;
/// <summary> The possible answers to the question </summary>
public Answer[] Choices;
/// <summary> If the question is mandatory or not. Not overwritten by presets </summary>
public bool IsMandatory;
}
[Serializable]
internal class Answer
{
/// <summary> Id (unique across answers) </summary>
public string Id;
/// <summary> What is displayed to the user </summary>
public string Title;
/// <summary> Optional description that will be shown in a tooltip </summary>
public string Description;
/// <summary> How picking this answer will impact the score of a given solution </summary>
public ScoreImpact[] ScoreImpacts;
}
[Serializable]
internal class ScoreImpact
{
/// <summary> Which score is impacted </summary>
public PossibleSolution Solution;
/// <summary> Absolute value to add or subtract from the target score</summary>
public float Score;
/// <summary> A comment displayed to the user as for why this score is impacted </summary>
public string Comment;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7ca5cd79ef39442e599efb761abcbaac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace Unity.Multiplayer.Center.Questionnaire
{
/// <summary>
/// Because of the "questionnaire" extension, the default inspector is not shown.
/// Double clicking on the asset will open this custom inspector (in debug mode), which has a way to force saving
/// the asset.
/// </summary>
[CustomEditor(typeof(QuestionnaireObject))]
internal class QuestionnaireEditor : Editor
{
public override VisualElement CreateInspectorGUI()
{
var so = new SerializedObject(target);
var root = new VisualElement();
var questionnaire = (QuestionnaireObject) target;
root.Add(new Button(() => questionnaire.ForceReload() ){text = "Load Changes From Disk", tooltip = "Use when editing with external editor."});
root.Add(new Button(() => questionnaire.ForceSave() ){text = "Save local changes", tooltip = "Use when editing in inspector."});
var inspector = new PropertyField(so.FindProperty("Questionnaire"));
root.Add(inspector);
return root;
}
[OnOpenAsset(1)]
public static bool OpenMyCustomAsset(int instanceID, int line)
{
if (!EditorPrefs.GetBool("DeveloperMode")) return false;
var asset = EditorUtility.InstanceIDToObject(instanceID);
var path = AssetDatabase.GetAssetPath(asset);
if(string.IsNullOrEmpty(path) || !path.EndsWith("questionnaire"))
return false;
Selection.activeObject = QuestionnaireObject.instance;
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5d117d33f89d41cd9123918383c9b981
timeCreated: 1701771946

View File

@@ -0,0 +1,28 @@
using System;
using UnityEditor;
using UnityEngine;
namespace Unity.Multiplayer.Center.Questionnaire
{
/// <summary>
/// The questionnaire scriptable object, used to store and edit the data
/// </summary>
[FilePath("Packages/com.unity.multiplayer.center/Editor/Questionnaire/Questionnaire.questionnaire", FilePathAttribute.Location.ProjectFolder)]
internal class QuestionnaireObject : ScriptableSingleton<QuestionnaireObject>
{
public QuestionnaireData Questionnaire;
public void ForceReload()
{
DestroyImmediate(QuestionnaireObject.instance);
var questions = QuestionnaireObject.instance.Questionnaire;
}
public void ForceSave()
{
base.Save(saveAsText:true);
AssetDatabase.Refresh();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3677cd9ede84e78b2fff6331c6301da
timeCreated: 1695304313

View File

@@ -0,0 +1,63 @@
using System;
using Unity.Multiplayer.Center.Common;
using UnityEditor;
using UnityEngine;
namespace Unity.Multiplayer.Center.Questionnaire
{
/// <summary>
/// The unity object that contains the current choices of the user.
/// </summary>
[FilePath("Assets/UserChoices.choices", FilePathAttribute.Location.ProjectFolder)]
internal class UserChoicesObject : ScriptableSingleton<UserChoicesObject>
{
/// <summary>
/// The version of the questionnaire the answers correspond to.
/// </summary>
public string QuestionnaireVersion;
/// <summary>
/// The answers of the user in the Game specs questionnaire.
/// </summary>
public AnswerData UserAnswers = new();
/// <summary>
/// Current preset selected by the user.
/// </summary>
public Preset Preset;
/// <summary>
/// The main selections made by the user in the recommendation tab.
/// </summary>
public SelectedSolutionsData SelectedSolutions;
/// <summary>
/// Raised when the SelectedSolutions changes
/// </summary>
public event Action OnSolutionSelectionChanged;
/// <summary>
/// Set the user selection and calls OnSelectionChanged if needed
/// </summary>
/// <param name="hostingModel">The selected hosting model</param>
/// <param name="netcodeSolution">The selected netcode solution</param>
internal void SetUserSelection(SelectedSolutionsData.HostingModel hostingModel, SelectedSolutionsData.NetcodeSolution netcodeSolution)
{
SelectedSolutions.SelectedHostingModel = hostingModel;
SelectedSolutions.SelectedNetcodeSolution = netcodeSolution;
OnSolutionSelectionChanged?.Invoke();
}
/// <summary>
/// Save to disk (see filepath)
/// </summary>
internal void Save()
{
QuestionnaireVersion = QuestionnaireObject.instance.Questionnaire.Version;
this.Save(true);
}
internal string FilePath => GetFilePath();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 18cde282a8d045bf9d245fdcfaa7271b
timeCreated: 1695397271