test
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9beb5106d424f23902054f76d4b673d
|
||||
timeCreated: 1695634195
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3595bcaa1af84e6d92e07f0258a4256c
|
||||
timeCreated: 1699627067
|
@@ -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
|
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a659150180ae3489ba41c71780ba3779
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ca5cd79ef39442e599efb761abcbaac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d117d33f89d41cd9123918383c9b981
|
||||
timeCreated: 1701771946
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3677cd9ede84e78b2fff6331c6301da
|
||||
timeCreated: 1695304313
|
@@ -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();
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18cde282a8d045bf9d245fdcfaa7271b
|
||||
timeCreated: 1695397271
|
Reference in New Issue
Block a user