Photon Fusion

dev-ali
Ali Sharoz 2 months ago
parent 764f18415d
commit 8e0aac9c3a

@ -0,0 +1,74 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Fusion;
using System.Linq;
using System.Threading.Tasks;
using Fusion.Sockets;
using System;
public class NetworkRunnerHandler : MonoBehaviour
{
//public NetworkRunner networkRunnerPrefab;
//NetworkRunner networkRunner;
//private void Awake()
//{
// NetworkRunner networkRunnerInScene = FindObjectOfType<NetworkRunner>();
// if (networkRunnerInScene != null)
// {
// networkRunner = networkRunnerInScene;
// }
//}
//private void Start()
//{
// //if (networkRunner == null)
// //{
// // networkRunner = Instantiate(networkRunnerPrefab);
// // networkRunner.name = "Network Runner";
// // var clientTask=InitializeNetworkRunner()
// // }
//}
//public void StartHostMigration(HostMigrationToken hostMigrationToken)
//{
// networkRunner=Instantiate(networkRunnerPrefab);
// networkRunner.name = "Network Runner - Migrated";
// var clientTask=InitializeNetworkRunnerHostMigration(networkRunner,hostMigrationToken);
//}
//protected virtual Task InitializeNetworkRunnerHostMigration(NetworkRunner runner,HostMigrationToken hostMigrationToken)
//{
// var sceneManager = GetSceneManager(runner);
// runner.ProvideInput = true;
// return runner.StartGame(new StartGameArgs
// {
// SceneManager = sceneManager,
// HostMigrationToken = hostMigrationToken,
// HostMigrationResume = HostMigrationResume,
// });
//}
//INetworkSceneManager GetSceneManager(NetworkRunner runner)
//{
// var sceneManager=runner.GetComponents(typeof(MonoBehaviour)).OfType<INetworkSceneManager>().FirstOrDefault();
// if (sceneManager == null)
// {
// sceneManager=runner.gameObject.AddComponent<NetworkSceneManagerDefault>();
// }
// return sceneManager;
//}
//protected virtual Task InitializeNetworkRunner(NetworkRunner runner, GameMode gameMode,string sessionName, byte[] connectionToken, NetAddress address, SceneRef scene, Action<NetworkRunner> action)
//{
// var sceneManager=GetSceneManager(runner);
// runner.ProvideInput = true;
// return runner.StartGame(new StartGameArgs
// {
// GameMode=gameMode,
// Address=address,
// Scene=scene,
// SessionName=sessionName,
// SceneManager=sceneManager,
// CustomLobbyName="OurLobbyId",
// ConnectionToken=connectionToken,
// });
//}
}

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

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ac2bb7763801bf046bdf695eb96645f0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a31ea7d0315594440839cdb0db6bc411
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8b14e706b1e7cb044b23837e8a70cad9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

@ -0,0 +1,22 @@
using UnityEditor;
namespace ParrelSync
{
[InitializeOnLoad]
public class EditorQuit
{
/// <summary>
/// Is editor being closed
/// </summary>
static public bool IsQuiting { get; private set; }
static void Quit()
{
IsQuiting = true;
}
static EditorQuit()
{
IsQuiting = false;
EditorApplication.quitting += Quit;
}
}
}

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

@ -0,0 +1,34 @@
using UnityEditor;
using UnityEngine;
namespace ParrelSync
{
/// <summary>
/// For preventing assets being modified from the clone instance.
/// </summary>
public class ParrelSyncAssetModificationProcessor : UnityEditor.AssetModificationProcessor
{
public static string[] OnWillSaveAssets(string[] paths)
{
if (ClonesManager.IsClone() && Preferences.AssetModPref.Value)
{
if (paths != null && paths.Length > 0 && !EditorQuit.IsQuiting)
{
EditorUtility.DisplayDialog(
ClonesManager.ProjectName + ": Asset modifications saving detected and blocked",
"Asset modifications saving are blocked in the clone instance. \n\n" +
"This is a clone of the original project. \n" +
"Making changes to asset files via the clone editor is not recommended. \n" +
"Please use the original editor window if you want to make changes to the project files.",
"ok"
);
foreach (var path in paths)
{
Debug.Log("Attempting to save " + path + " are blocked.");
}
}
return new string[0] { };
}
return paths;
}
}
}

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

@ -0,0 +1,692 @@
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.IO;
using Debug = UnityEngine.Debug;
namespace ParrelSync
{
/// <summary>
/// Contains all required methods for creating a linked clone of the Unity project.
/// </summary>
public class ClonesManager
{
/// <summary>
/// Name used for an identifying file created in the clone project directory.
/// </summary>
/// <remarks>
/// (!) Do not change this after the clone was created, because then connection will be lost.
/// </remarks>
public const string CloneFileName = ".clone";
/// <summary>
/// Suffix added to the end of the project clone name when it is created.
/// </summary>
/// <remarks>
/// (!) Do not change this after the clone was created, because then connection will be lost.
/// </remarks>
public const string CloneNameSuffix = "_clone";
public const string ProjectName = "ParrelSync";
/// <summary>
/// The maximum number of clones
/// </summary>
public const int MaxCloneProjectCount = 10;
/// <summary>
/// Name of the file for storing clone's argument.
/// </summary>
public const string ArgumentFileName = ".parrelsyncarg";
/// <summary>
/// Default argument of the new clone
/// </summary>
public const string DefaultArgument = "client";
#region Managing clones
/// <summary>
/// Creates clone from the project currently open in Unity Editor.
/// </summary>
/// <returns></returns>
public static Project CreateCloneFromCurrent()
{
if (IsClone())
{
Debug.LogError("This project is already a clone. Cannot clone it.");
return null;
}
string currentProjectPath = ClonesManager.GetCurrentProjectPath();
return ClonesManager.CreateCloneFromPath(currentProjectPath);
}
/// <summary>
/// Creates clone of the project located at the given path.
/// </summary>
/// <param name="sourceProjectPath"></param>
/// <returns></returns>
public static Project CreateCloneFromPath(string sourceProjectPath)
{
Project sourceProject = new Project(sourceProjectPath);
string cloneProjectPath = null;
//Find available clone suffix id
for (int i = 0; i < MaxCloneProjectCount; i++)
{
string originalProjectPath = ClonesManager.GetCurrentProject().projectPath;
string possibleCloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i;
if (!Directory.Exists(possibleCloneProjectPath))
{
cloneProjectPath = possibleCloneProjectPath;
break;
}
}
if (string.IsNullOrEmpty(cloneProjectPath))
{
Debug.LogError("The number of cloned projects has reach its limit. Limit: " + MaxCloneProjectCount);
return null;
}
Project cloneProject = new Project(cloneProjectPath);
Debug.Log("Start cloning project, original project: " + sourceProject + ", clone project: " + cloneProject);
ClonesManager.CreateProjectFolder(cloneProject);
//Copy Folders
Debug.Log("Library copy: " + cloneProject.libraryPath);
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, cloneProject.libraryPath,
"Cloning Project Library '" + sourceProject.name + "'. ");
Debug.Log("Packages copy: " + cloneProject.libraryPath);
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.packagesPath, cloneProject.packagesPath,
"Cloning Project Packages '" + sourceProject.name + "'. ");
//Link Folders
ClonesManager.LinkFolders(sourceProject.assetPath, cloneProject.assetPath);
ClonesManager.LinkFolders(sourceProject.projectSettingsPath, cloneProject.projectSettingsPath);
ClonesManager.LinkFolders(sourceProject.autoBuildPath, cloneProject.autoBuildPath);
ClonesManager.LinkFolders(sourceProject.localPackages, cloneProject.localPackages);
//Optional Link Folders
var optionalLinkPaths = Preferences.OptionalSymbolicLinkFolders.GetStoredValue();
var projectSettings = ParrelSyncProjectSettings.GetSerializedSettings();
var projectSettingsProperty = projectSettings.FindProperty("m_OptionalSymbolicLinkFolders");
if (projectSettingsProperty is { isArray: true, arrayElementType: "string" })
{
for (var i = 0; i < projectSettingsProperty.arraySize; ++i)
{
optionalLinkPaths.Add(projectSettingsProperty.GetArrayElementAtIndex(i).stringValue);
}
}
foreach (var path in optionalLinkPaths)
{
var sourceOptionalPath = sourceProjectPath + path;
var cloneOptionalPath = cloneProjectPath + path;
LinkFolders(sourceOptionalPath, cloneOptionalPath);
}
ClonesManager.RegisterClone(cloneProject);
return cloneProject;
}
/// <summary>
/// Registers a clone by placing an identifying ".clone" file in its root directory.
/// </summary>
/// <param name="cloneProject"></param>
private static void RegisterClone(Project cloneProject)
{
/// Add clone identifier file.
string identifierFile = Path.Combine(cloneProject.projectPath, ClonesManager.CloneFileName);
File.Create(identifierFile).Dispose();
//Add argument file with default argument
string argumentFilePath = Path.Combine(cloneProject.projectPath, ClonesManager.ArgumentFileName);
File.WriteAllText(argumentFilePath, DefaultArgument, System.Text.Encoding.UTF8);
/// Add collabignore.txt to stop the clone from messing with Unity Collaborate if it's enabled. Just in case.
string collabignoreFile = Path.Combine(cloneProject.projectPath, "collabignore.txt");
File.WriteAllText(collabignoreFile, "*"); /// Make it ignore ALL files in the clone.
}
/// <summary>
/// Opens a project located at the given path (if one exists).
/// </summary>
/// <param name="projectPath"></param>
public static void OpenProject(string projectPath)
{
if (!Directory.Exists(projectPath))
{
Debug.LogError("Cannot open the project - provided folder (" + projectPath + ") does not exist.");
return;
}
if (projectPath == ClonesManager.GetCurrentProjectPath())
{
Debug.LogError("Cannot open the project - it is already open.");
return;
}
//Validate (and update if needed) the "Packages" folder before opening clone project to ensure the clone project will have the
//same "compiling environment" as the original project
ValidateCopiedFoldersIntegrity.ValidateFolder(projectPath, GetOriginalProjectPath(), "Packages");
string fileName = GetApplicationPath();
string args = "-projectPath \"" + projectPath + "\"";
Debug.Log("Opening project \"" + fileName + " " + args + "\"");
ClonesManager.StartHiddenConsoleProcess(fileName, args);
}
private static string GetApplicationPath()
{
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
return EditorApplication.applicationPath;
case RuntimePlatform.OSXEditor:
return EditorApplication.applicationPath + "/Contents/MacOS/Unity";
case RuntimePlatform.LinuxEditor:
return EditorApplication.applicationPath;
default:
throw new System.NotImplementedException("Platform has not supported yet ;(");
}
}
/// <summary>
/// Is this project being opened by an Unity editor?
/// </summary>
/// <param name="projectPath"></param>
/// <returns></returns>
public static bool IsCloneProjectRunning(string projectPath)
{
//Determine whether it is opened in another instance by checking the UnityLockFile
string UnityLockFilePath = new string[] { projectPath, "Temp", "UnityLockfile" }
.Aggregate(Path.Combine);
switch (Application.platform)
{
case (RuntimePlatform.WindowsEditor):
//Windows editor will lock "UnityLockfile" file when project is being opened.
//Sometime, for instance: windows editor crash, the "UnityLockfile" will not be deleted even the project
//isn't being opened, so a check to the "UnityLockfile" lock status may be necessary.
if (Preferences.AlsoCheckUnityLockFileStaPref.Value)
return File.Exists(UnityLockFilePath) && FileUtilities.IsFileLocked(UnityLockFilePath);
else
return File.Exists(UnityLockFilePath);
case (RuntimePlatform.OSXEditor):
//Mac editor won't lock "UnityLockfile" file when project is being opened
return File.Exists(UnityLockFilePath);
case (RuntimePlatform.LinuxEditor):
return File.Exists(UnityLockFilePath);
default:
throw new System.NotImplementedException("IsCloneProjectRunning: Unsupport Platfrom: " + Application.platform);
}
}
/// <summary>
/// Deletes the clone of the currently open project, if such exists.
/// </summary>
public static void DeleteClone(string cloneProjectPath)
{
/// Clone won't be able to delete itself.
if (ClonesManager.IsClone()) return;
///Extra precautions.
if (cloneProjectPath == string.Empty) return;
if (cloneProjectPath == ClonesManager.GetOriginalProjectPath()) return;
//Check what OS is
string identifierFile;
string args;
switch (Application.platform)
{
case (RuntimePlatform.WindowsEditor):
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
//The argument file will be deleted first at the beginning of the project deletion process
//to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.)
//If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed.
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
File.Delete(identifierFile);
args = "/c " + @"rmdir /s/q " + string.Format("\"{0}\"", cloneProjectPath);
StartHiddenConsoleProcess("cmd.exe", args);
break;
case (RuntimePlatform.OSXEditor):
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
//The argument file will be deleted first at the beginning of the project deletion process
//to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.)
//If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed.
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
File.Delete(identifierFile);
FileUtil.DeleteFileOrDirectory(cloneProjectPath);
break;
case (RuntimePlatform.LinuxEditor):
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
File.Delete(identifierFile);
FileUtil.DeleteFileOrDirectory(cloneProjectPath);
break;
default:
Debug.LogWarning("Not in a known editor. Where are you!?");
break;
}
}
#endregion
#region Creating project folders
/// <summary>
/// Creates an empty folder using data in the given Project object
/// </summary>
/// <param name="project"></param>
public static void CreateProjectFolder(Project project)
{
string path = project.projectPath;
Debug.Log("Creating new empty folder at: " + path);
Directory.CreateDirectory(path);
}
/// <summary>
/// Copies the full contents of the unity library. We want to do this to avoid the lengthy re-serialization of the whole project when it opens up the clone.
/// </summary>
/// <param name="sourceProject"></param>
/// <param name="destinationProject"></param>
[System.Obsolete]
public static void CopyLibraryFolder(Project sourceProject, Project destinationProject)
{
if (Directory.Exists(destinationProject.libraryPath))
{
Debug.LogWarning("Library copy: destination path already exists! ");
return;
}
Debug.Log("Library copy: " + destinationProject.libraryPath);
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, destinationProject.libraryPath,
"Cloning project '" + sourceProject.name + "'. ");
}
#endregion
#region Creating symlinks
/// <summary>
/// Creates a symlink between destinationPath and sourcePath (Mac version).
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="destinationPath"></param>
private static void CreateLinkMac(string sourcePath, string destinationPath)
{
sourcePath = sourcePath.Replace(" ", "\\ ");
destinationPath = destinationPath.Replace(" ", "\\ ");
var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath);
Debug.Log("Mac hard link " + command);
ClonesManager.ExecuteBashCommand(command);
}
/// <summary>
/// Creates a symlink between destinationPath and sourcePath (Linux version).
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="destinationPath"></param>
private static void CreateLinkLinux(string sourcePath, string destinationPath)
{
sourcePath = sourcePath.Replace(" ", "\\ ");
destinationPath = destinationPath.Replace(" ", "\\ ");
var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath);
Debug.Log("Linux Symlink " + command);
ClonesManager.ExecuteBashCommand(command);
}
/// <summary>
/// Creates a symlink between destinationPath and sourcePath (Windows version).
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="destinationPath"></param>
private static void CreateLinkWin(string sourcePath, string destinationPath)
{
string cmd = "/C mklink /J " + string.Format("\"{0}\" \"{1}\"", destinationPath, sourcePath);
Debug.Log("Windows junction: " + cmd);
ClonesManager.StartHiddenConsoleProcess("cmd.exe", cmd);
}
//TODO(?) avoid terminal calls and use proper api stuff. See below for windows!
////https://docs.microsoft.com/en-us/windows/desktop/api/ioapiset/nf-ioapiset-deviceiocontrol
//[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
//private static extern bool DeviceIoControl(System.IntPtr hDevice, uint dwIoControlCode,
// System.IntPtr InBuffer, int nInBufferSize,
// System.IntPtr OutBuffer, int nOutBufferSize,
// out int pBytesReturned, System.IntPtr lpOverlapped);
/// <summary>
/// Create a link / junction from the original project to it's clone.
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="destinationPath"></param>
public static void LinkFolders(string sourcePath, string destinationPath)
{
if ((Directory.Exists(destinationPath) == false) && (Directory.Exists(sourcePath) == true))
{
switch (Application.platform)
{
case (RuntimePlatform.WindowsEditor):
CreateLinkWin(sourcePath, destinationPath);
break;
case (RuntimePlatform.OSXEditor):
CreateLinkMac(sourcePath, destinationPath);
break;
case (RuntimePlatform.LinuxEditor):
CreateLinkLinux(sourcePath, destinationPath);
break;
default:
Debug.LogWarning("Not in a known editor. Application.platform: " + Application.platform);
break;
}
}
else
{
Debug.LogWarning("Skipping Asset link, it already exists: " + destinationPath);
}
}
#endregion
#region Utility methods
private static bool? isCloneFileExistCache = null;
/// <summary>
/// Returns true if the project currently open in Unity Editor is a clone.
/// </summary>
/// <returns></returns>
public static bool IsClone()
{
if (isCloneFileExistCache == null)
{
/// The project is a clone if its root directory contains an empty file named ".clone".
string cloneFilePath = Path.Combine(ClonesManager.GetCurrentProjectPath(), ClonesManager.CloneFileName);
isCloneFileExistCache = File.Exists(cloneFilePath);
}
return (bool)isCloneFileExistCache;
}
/// <summary>
/// Get the path to the current unityEditor project folder's info
/// </summary>
/// <returns></returns>
public static string GetCurrentProjectPath()
{
return Application.dataPath.Replace("/Assets", "");
}
/// <summary>
/// Return a project object that describes all the paths we need to clone it.
/// </summary>
/// <returns></returns>
public static Project GetCurrentProject()
{
string pathString = ClonesManager.GetCurrentProjectPath();
return new Project(pathString);
}
/// <summary>
/// Get the argument of this clone project.
/// If this is the original project, will return an empty string.
/// </summary>
/// <returns></returns>
public static string GetArgument()
{
string argument = "";
if (IsClone())
{
string argumentFilePath = Path.Combine(GetCurrentProjectPath(), ClonesManager.ArgumentFileName);
if (File.Exists(argumentFilePath))
{
argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8);
}
}
return argument;
}
/// <summary>
/// Returns the path to the original project.
/// If currently open project is the original, returns its own path.
/// If the original project folder cannot be found, retuns an empty string.
/// </summary>
/// <returns></returns>
public static string GetOriginalProjectPath()
{
if (IsClone())
{
/// If this is a clone...
/// Original project path can be deduced by removing the suffix from the clone's path.
string cloneProjectPath = ClonesManager.GetCurrentProject().projectPath;
int index = cloneProjectPath.LastIndexOf(ClonesManager.CloneNameSuffix);
if (index > 0)
{
string originalProjectPath = cloneProjectPath.Substring(0, index);
if (Directory.Exists(originalProjectPath)) return originalProjectPath;
}
return string.Empty;
}
else
{
/// If this is the original, we return its own path.
return ClonesManager.GetCurrentProjectPath();
}
}
/// <summary>
/// Returns all clone projects path.
/// </summary>
/// <returns></returns>
public static List<string> GetCloneProjectsPath()
{
List<string> projectsPath = new List<string>();
for (int i = 0; i < MaxCloneProjectCount; i++)
{
string originalProjectPath = ClonesManager.GetCurrentProject().projectPath;
string cloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i;
if (Directory.Exists(cloneProjectPath))
projectsPath.Add(cloneProjectPath);
}
return projectsPath;
}
/// <summary>
/// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
/// </summary>
/// <param name="source">Directory to be copied.</param>
/// <param name="destination">Destination directory (created automatically if needed).</param>
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
public static void CopyDirectoryWithProgressBar(string sourcePath, string destinationPath,
string progressBarPrefix = "")
{
var source = new DirectoryInfo(sourcePath);
var destination = new DirectoryInfo(destinationPath);
long totalBytes = 0;
long copiedBytes = 0;
ClonesManager.CopyDirectoryWithProgressBarRecursive(source, destination, ref totalBytes, ref copiedBytes,
progressBarPrefix);
EditorUtility.ClearProgressBar();
}
/// <summary>
/// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
/// Same as the previous method, but uses recursion to copy all nested folders as well.
/// </summary>
/// <param name="source">Directory to be copied.</param>
/// <param name="destination">Destination directory (created automatically if needed).</param>
/// <param name="totalBytes">Total bytes to be copied. Calculated automatically, initialize at 0.</param>
/// <param name="copiedBytes">To track already copied bytes. Calculated automatically, initialize at 0.</param>
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
private static void CopyDirectoryWithProgressBarRecursive(DirectoryInfo source, DirectoryInfo destination,
ref long totalBytes, ref long copiedBytes, string progressBarPrefix = "")
{
/// Directory cannot be copied into itself.
if (source.FullName.ToLower() == destination.FullName.ToLower())
{
Debug.LogError("Cannot copy directory into itself.");
return;
}
/// Calculate total bytes, if required.
if (totalBytes == 0)
{
totalBytes = ClonesManager.GetDirectorySize(source, true, progressBarPrefix);
}
/// Create destination directory, if required.
if (!Directory.Exists(destination.FullName))
{
Directory.CreateDirectory(destination.FullName);
}
/// Copy all files from the source.
foreach (FileInfo file in source.GetFiles())
{
// Ensure file exists before continuing.
if (!file.Exists)
{
continue;
}
try
{
file.CopyTo(Path.Combine(destination.ToString(), file.Name), true);
}
catch (IOException)
{
/// Some files may throw IOException if they are currently open in Unity editor.
/// Just ignore them in such case.
}
/// Account the copied file size.
copiedBytes += file.Length;
/// Display the progress bar.
float progress = (float)copiedBytes / (float)totalBytes;
bool cancelCopy = EditorUtility.DisplayCancelableProgressBar(
progressBarPrefix + "Copying '" + source.FullName + "' to '" + destination.FullName + "'...",
"(" + (progress * 100f).ToString("F2") + "%) Copying file '" + file.Name + "'...",
progress);
if (cancelCopy) return;
}
/// Copy all nested directories from the source.
foreach (DirectoryInfo sourceNestedDir in source.GetDirectories())
{
DirectoryInfo nextDestingationNestedDir = destination.CreateSubdirectory(sourceNestedDir.Name);
ClonesManager.CopyDirectoryWithProgressBarRecursive(sourceNestedDir, nextDestingationNestedDir,
ref totalBytes, ref copiedBytes, progressBarPrefix);
}
}
/// <summary>
/// Calculates the size of the given directory. Displays a progress bar.
/// </summary>
/// <param name="directory">Directory, which size has to be calculated.</param>
/// <param name="includeNested">If true, size will include all nested directories.</param>
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
/// <returns>Size of the directory in bytes.</returns>
private static long GetDirectorySize(DirectoryInfo directory, bool includeNested = false,
string progressBarPrefix = "")
{
EditorUtility.DisplayProgressBar(progressBarPrefix + "Calculating size of directories...",
"Scanning '" + directory.FullName + "'...", 0f);
/// Calculate size of all files in directory.
long filesSize = directory.GetFiles().Sum((FileInfo file) => file.Exists ? file.Length : 0);
/// Calculate size of all nested directories.
long directoriesSize = 0;
if (includeNested)
{
IEnumerable<DirectoryInfo> nestedDirectories = directory.GetDirectories();
foreach (DirectoryInfo nestedDir in nestedDirectories)
{
directoriesSize += ClonesManager.GetDirectorySize(nestedDir, true, progressBarPrefix);
}
}
return filesSize + directoriesSize;
}
/// <summary>
/// Starts process in the system console, taking the given fileName and args.
/// </summary>
/// <param name="fileName"></param>
/// <param name="args"></param>
private static void StartHiddenConsoleProcess(string fileName, string args)
{
System.Diagnostics.Process.Start(fileName, args);
}
/// <summary>
/// Thanks to https://github.com/karl-/unity-symlink-utility/blob/master/SymlinkUtility.cs
/// </summary>
/// <param name="command"></param>
private static void ExecuteBashCommand(string command)
{
command = command.Replace("\"", "\"\"");
var proc = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = "-c \"" + command + "\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
using (proc)
{
proc.Start();
proc.WaitForExit();
if (!proc.StandardError.EndOfStream)
{
UnityEngine.Debug.LogError(proc.StandardError.ReadToEnd());
}
}
}
public static void OpenProjectInFileExplorer(string path)
{
System.Diagnostics.Process.Start(@path);
}
#endregion
}
}

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

@ -0,0 +1,198 @@
using UnityEngine;
using UnityEditor;
using System.IO;
namespace ParrelSync
{
/// <summary>
///Clones manager Unity editor window
/// </summary>
public class ClonesManagerWindow : EditorWindow
{
/// <summary>
/// Returns true if project clone exists.
/// </summary>
public bool isCloneCreated
{
get { return ClonesManager.GetCloneProjectsPath().Count >= 1; }
}
[MenuItem("ParrelSync/Clones Manager", priority = 0)]
private static void InitWindow()
{
ClonesManagerWindow window = (ClonesManagerWindow)EditorWindow.GetWindow(typeof(ClonesManagerWindow));
window.titleContent = new GUIContent("Clones Manager");
window.Show();
}
/// <summary>
/// For storing the scroll position of clones list
/// </summary>
Vector2 clonesScrollPos;
private void OnGUI()
{
/// If it is a clone project...
if (ClonesManager.IsClone())
{
//Find out the original project name and show the help box
string originalProjectPath = ClonesManager.GetOriginalProjectPath();
if (originalProjectPath == string.Empty)
{
/// If original project cannot be found, display warning message.
EditorGUILayout.HelpBox(
"This project is a clone, but the link to the original seems lost.\nYou have to manually open the original and create a new clone instead of this one.\n",
MessageType.Warning);
}
else
{
/// If original project is present, display some usage info.
EditorGUILayout.HelpBox(
"This project is a clone of the project '" + Path.GetFileName(originalProjectPath) + "'.\nIf you want to make changes the project files or manage clones, please open the original project through Unity Hub.",
MessageType.Info);
}
//Clone project custom argument.
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Arguments", GUILayout.Width(70));
if (GUILayout.Button("?", GUILayout.Width(20)))
{
Application.OpenURL(ExternalLinks.CustomArgumentHelpLink);
}
GUILayout.EndHorizontal();
string argumentFilePath = Path.Combine(ClonesManager.GetCurrentProjectPath(), ClonesManager.ArgumentFileName);
//Need to be careful with file reading / writing since it will effect the deletion of
// the clone project(The directory won't be fully deleted if there's still file inside being read or write).
//The argument file will be deleted first at the beginning of the project deletion process
//to prevent any further being read and write.
//Will need to take some extra cautious if want to change the design of how file editing is handled.
if (File.Exists(argumentFilePath))
{
string argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8);
string argumentTextAreaInput = EditorGUILayout.TextArea(argument,
GUILayout.Height(50),
GUILayout.MaxWidth(300)
);
File.WriteAllText(argumentFilePath, argumentTextAreaInput, System.Text.Encoding.UTF8);
}
else
{
EditorGUILayout.LabelField("No argument file found.");
}
}
else// If it is an original project...
{
if (isCloneCreated)
{
GUILayout.BeginVertical("HelpBox");
GUILayout.Label("Clones of this Project");
//List all clones
clonesScrollPos =
EditorGUILayout.BeginScrollView(clonesScrollPos);
var cloneProjectsPath = ClonesManager.GetCloneProjectsPath();
for (int i = 0; i < cloneProjectsPath.Count; i++)
{
GUILayout.BeginVertical("GroupBox");
string cloneProjectPath = cloneProjectsPath[i];
bool isOpenInAnotherInstance = ClonesManager.IsCloneProjectRunning(cloneProjectPath);
if (isOpenInAnotherInstance == true)
EditorGUILayout.LabelField("Clone " + i + " (Running)", EditorStyles.boldLabel);
else
EditorGUILayout.LabelField("Clone " + i);
GUILayout.BeginHorizontal();
EditorGUILayout.TextField("Clone project path", cloneProjectPath, EditorStyles.textField);
if (GUILayout.Button("View Folder", GUILayout.Width(80)))
{
ClonesManager.OpenProjectInFileExplorer(cloneProjectPath);
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Arguments", GUILayout.Width(70));
if (GUILayout.Button("?", GUILayout.Width(20)))
{
Application.OpenURL(ExternalLinks.CustomArgumentHelpLink);
}
GUILayout.EndHorizontal();
string argumentFilePath = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
//Need to be careful with file reading/writing since it will effect the deletion of
//the clone project(The directory won't be fully deleted if there's still file inside being read or write).
//The argument file will be deleted first at the beginning of the project deletion process
//to prevent any further being read and write.
//Will need to take some extra cautious if want to change the design of how file editing is handled.
if (File.Exists(argumentFilePath))
{
string argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8);
string argumentTextAreaInput = EditorGUILayout.TextArea(argument,
GUILayout.Height(50),
GUILayout.MaxWidth(300)
);
File.WriteAllText(argumentFilePath, argumentTextAreaInput, System.Text.Encoding.UTF8);
}
else
{
EditorGUILayout.LabelField("No argument file found.");
}
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUI.BeginDisabledGroup(isOpenInAnotherInstance);
if (GUILayout.Button("Open in New Editor"))
{
ClonesManager.OpenProject(cloneProjectPath);
}
GUILayout.BeginHorizontal();
if (GUILayout.Button("Delete"))
{
bool delete = EditorUtility.DisplayDialog(
"Delete the clone?",
"Are you sure you want to delete the clone project '" + ClonesManager.GetCurrentProject().name + "_clone'?",
"Delete",
"Cancel");
if (delete)
{
ClonesManager.DeleteClone(cloneProjectPath);
}
}
GUILayout.EndHorizontal();
EditorGUI.EndDisabledGroup();
GUILayout.EndVertical();
}
EditorGUILayout.EndScrollView();
if (GUILayout.Button("Add new clone"))
{
ClonesManager.CreateCloneFromCurrent();
}
GUILayout.EndVertical();
GUILayout.FlexibleSpace();
}
else
{
/// If no clone created yet, we must create it.
EditorGUILayout.HelpBox("No project clones found. Create a new one!", MessageType.Info);
if (GUILayout.Button("Create new clone"))
{
ClonesManager.CreateCloneFromCurrent();
}
}
}
}
}
}

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

@ -0,0 +1,13 @@
namespace ParrelSync
{
public class ExternalLinks
{
public const string RemoteVersionURL = "https://raw.githubusercontent.com/VeriorPies/ParrelSync/master/VERSION.txt";
public const string Releases = "https://github.com/VeriorPies/ParrelSync/releases";
public const string CustomArgumentHelpLink = "https://github.com/VeriorPies/ParrelSync/wiki/Argument";
public const string GitHubHome = "https://github.com/VeriorPies/ParrelSync/";
public const string GitHubIssue = "https://github.com/VeriorPies/ParrelSync/issues";
public const string FAQ = "https://github.com/VeriorPies/ParrelSync/wiki/Troubleshooting-&-FAQs";
}
}

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

@ -0,0 +1,31 @@
using System.IO;
using UnityEngine;
namespace ParrelSync
{
public class FileUtilities
{
public static bool IsFileLocked(string path)
{
FileInfo file = new FileInfo(path);
try
{
using (FileStream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None))
{
stream.Close();
}
}
catch (IOException)
{
//the file is unavailable because it is:
//still being written to
//or being processed by another thread
//or does not exist (has already been processed)
return true;
}
//file is not locked
return false;
}
}
}

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

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 74a7aa389726f964ab34c52e208c2a43
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

@ -0,0 +1,78 @@
namespace ParrelSync.NonCore
{
using UnityEditor;
using UnityEngine;
/// <summary>
/// A simple script to display feedback/star dialog after certain time of project being opened/re-compiled.
/// Will only pop-up once unless "Remind me next time" are chosen.
/// Removing this file from project wont effect any other functions.
/// </summary>
[InitializeOnLoad]
public class AskFeedbackDialog
{
const string InitializeOnLoadCountKey = "ParrelSync_InitOnLoadCount", StopShowingKey = "ParrelSync_StopShowFeedBack";
static AskFeedbackDialog()
{
if (EditorPrefs.HasKey(StopShowingKey)) { return; }
int InitializeOnLoadCount = EditorPrefs.GetInt(InitializeOnLoadCountKey, 0);
if (InitializeOnLoadCount > 20)
{
ShowDialog();
}
else
{
EditorPrefs.SetInt(InitializeOnLoadCountKey, InitializeOnLoadCount + 1);
}
}
//[MenuItem("ParrelSync/(Debug)Show AskFeedbackDialog ")]
private static void ShowDialog()
{
int option = EditorUtility.DisplayDialogComplex("Do you like " + ParrelSync.ClonesManager.ProjectName + "?",
"Do you like " + ParrelSync.ClonesManager.ProjectName + "?\n" +
"If so, please don't hesitate to star it on GitHub and contribute to the project!",
"Star on GitHub",
"Close",
"Remind me next time"
);
switch (option)
{
// First parameter.
case 0:
Debug.Log("AskFeedbackDialog: Star on GitHub selected");
EditorPrefs.SetBool(StopShowingKey, true);
EditorPrefs.DeleteKey(InitializeOnLoadCountKey);
Application.OpenURL(ExternalLinks.GitHubHome);
break;
// Second parameter.
case 1:
Debug.Log("AskFeedbackDialog: Close and never show again.");
EditorPrefs.SetBool(StopShowingKey, true);
EditorPrefs.DeleteKey(InitializeOnLoadCountKey);
break;
// Third parameter.
case 2:
Debug.Log("AskFeedbackDialog: Remind me next time");
EditorPrefs.SetInt(InitializeOnLoadCountKey, 0);
break;
default:
//Debug.Log("Close windows.");
break;
}
}
///// <summary>
///// For debug purpose
///// </summary>
//[MenuItem("ParrelSync/(Debug)Delete AskFeedbackDialog keys")]
//private static void DebugDeleteAllKeys()
//{
// EditorPrefs.DeleteKey(InitializeOnLoadCountKey);
// EditorPrefs.DeleteKey(StopShowingKey);
// Debug.Log("AskFeedbackDialog keys deleted");
//}
}
}

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

@ -0,0 +1,26 @@
namespace ParrelSync.NonCore
{
using UnityEditor;
using UnityEngine;
public class OtherMenuItem
{
[MenuItem("ParrelSync/GitHub/View this project on GitHub", priority = 10)]
private static void OpenGitHub()
{
Application.OpenURL(ExternalLinks.GitHubHome);
}
[MenuItem("ParrelSync/GitHub/View FAQ", priority = 11)]
private static void OpenFAQ()
{
Application.OpenURL(ExternalLinks.FAQ);
}
[MenuItem("ParrelSync/GitHub/View Issues", priority = 12)]
private static void OpenGitHubIssues()
{
Application.OpenURL(ExternalLinks.GitHubIssue);
}
}
}

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

@ -0,0 +1,140 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace ParrelSync
{
// With ScriptableObject derived classes, .cs and .asset filenames MUST be identical
public class ParrelSyncProjectSettings : ScriptableObject
{
private const string ParrelSyncScriptableObjectsDirectory = "Assets/Plugins/ParrelSync/ScriptableObjects";
private const string ParrelSyncSettingsPath = ParrelSyncScriptableObjectsDirectory + "/" +
nameof(ParrelSyncProjectSettings) + ".asset";
[SerializeField]
[HideInInspector]
private List<string> m_OptionalSymbolicLinkFolders;
public const string NameOfOptionalSymbolicLinkFolders = nameof(m_OptionalSymbolicLinkFolders);
private static ParrelSyncProjectSettings GetOrCreateSettings()
{
ParrelSyncProjectSettings projectSettings;
if (File.Exists(ParrelSyncSettingsPath))
{
projectSettings = AssetDatabase.LoadAssetAtPath<ParrelSyncProjectSettings>(ParrelSyncSettingsPath);
if (projectSettings == null)
Debug.LogError("File Exists, but failed to load: " + ParrelSyncSettingsPath);
return projectSettings;
}
projectSettings = CreateInstance<ParrelSyncProjectSettings>();
projectSettings.m_OptionalSymbolicLinkFolders = new List<string>();
if (!Directory.Exists(ParrelSyncScriptableObjectsDirectory))
{
Directory.CreateDirectory(ParrelSyncScriptableObjectsDirectory);
}
AssetDatabase.CreateAsset(projectSettings, ParrelSyncSettingsPath);
AssetDatabase.SaveAssets();
return projectSettings;
}
public static SerializedObject GetSerializedSettings()
{
return new SerializedObject(GetOrCreateSettings());
}
}
public class ParrelSyncSettingsProvider : SettingsProvider
{
private const string MenuLocationInProjectSettings = "Project/ParrelSync";
private SerializedObject _parrelSyncProjectSettings;
private class Styles
{
public static readonly GUIContent SymlinkSectionHeading = new GUIContent("Optional Folders to Symbolically Link");
}
private ParrelSyncSettingsProvider(string path, SettingsScope scope = SettingsScope.User)
: base(path, scope)
{
}
public override void OnActivate(string searchContext, VisualElement rootElement)
{
// This function is called when the user clicks on the ParrelSyncSettings element in the Settings window.
_parrelSyncProjectSettings = ParrelSyncProjectSettings.GetSerializedSettings();
}
public override void OnGUI(string searchContext)
{
var property = _parrelSyncProjectSettings.FindProperty(ParrelSyncProjectSettings.NameOfOptionalSymbolicLinkFolders);
if (property is null || !property.isArray || property.arrayElementType != "string")
return;
var optionalFolderPaths = new List<string>(property.arraySize);
for (var i = 0; i < property.arraySize; ++i)
{
optionalFolderPaths.Add(property.GetArrayElementAtIndex(i).stringValue);
}
optionalFolderPaths.Add("");
GUILayout.BeginVertical("GroupBox");
GUILayout.Label(Styles.SymlinkSectionHeading);
GUILayout.Space(5);
var projectPath = ClonesManager.GetCurrentProjectPath();
var optionalFolderPathsIsDirty = false;
for (var i = 0; i < optionalFolderPaths.Count; ++i)
{
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField(optionalFolderPaths[i], EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select", GUILayout.Width(60)))
{
var result = EditorUtility.OpenFolderPanel("Select Folder to Symbolically Link...", "", "");
if (result.Contains(projectPath))
{
optionalFolderPaths[i] = result.Replace(projectPath, "");
optionalFolderPathsIsDirty = true;
}
else if (result != "")
{
Debug.LogWarning("Symbolic Link folder must be within the project directory");
}
}
if (GUILayout.Button("Clear", GUILayout.Width(60)))
{
optionalFolderPaths[i] = "";
optionalFolderPathsIsDirty = true;
}
GUILayout.EndHorizontal();
}
GUILayout.EndVertical();
if (!optionalFolderPathsIsDirty)
return;
optionalFolderPaths.RemoveAll(str => str == "");
property.arraySize = optionalFolderPaths.Count;
for (var i = 0; i < property.arraySize; ++i)
{
property.GetArrayElementAtIndex(i).stringValue = optionalFolderPaths[i];
}
_parrelSyncProjectSettings.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
}
// Register the SettingsProvider
[SettingsProvider]
public static SettingsProvider CreateParrelSyncSettingsProvider()
{
return new ParrelSyncSettingsProvider(MenuLocationInProjectSettings, SettingsScope.Project)
{
keywords = GetSearchKeywordsFromGUIContentProperties<Styles>()
};
}
}
}

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

@ -0,0 +1,215 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
namespace ParrelSync
{
/// <summary>
/// To add value caching for <see cref="EditorPrefs"/> functions
/// </summary>
public class BoolPreference
{
public string key { get; private set; }
public bool defaultValue { get; private set; }
public BoolPreference(string key, bool defaultValue)
{
this.key = key;
this.defaultValue = defaultValue;
}
private bool? valueCache = null;
public bool Value
{
get
{
if (valueCache == null)
valueCache = EditorPrefs.GetBool(key, defaultValue);
return (bool)valueCache;
}
set
{
if (valueCache == value)
return;
EditorPrefs.SetBool(key, value);
valueCache = value;
Debug.Log("Editor preference updated. key: " + key + ", value: " + value);
}
}
public void ClearValue()
{
EditorPrefs.DeleteKey(key);
valueCache = null;
}
}
/// <summary>
/// To add value caching for <see cref="EditorPrefs"/> functions
/// </summary>
public class ListOfStringsPreference
{
private static string serializationToken = "|||";
public string Key { get; private set; }
public ListOfStringsPreference(string key)
{
Key = key;
}
public List<string> GetStoredValue()
{
return this.Deserialize(EditorPrefs.GetString(Key));
}
public void SetStoredValue(List<string> strings)
{
EditorPrefs.SetString(Key, this.Serialize(strings));
}
public void ClearStoredValue()
{
EditorPrefs.DeleteKey(Key);
}
public string Serialize(List<string> data)
{
string result = string.Empty;
foreach (var item in data)
{
if (item.Contains(serializationToken))
{
Debug.LogError("Unable to serialize this value ["+item+"], it contains the serialization token ["+serializationToken+"]");
continue;
}
result += item + serializationToken;
}
return result;
}
public List<string> Deserialize(string data)
{
return data.Split(serializationToken).ToList();
}
}
public class Preferences : EditorWindow
{
[MenuItem("ParrelSync/Preferences", priority = 1)]
private static void InitWindow()
{
Preferences window = (Preferences)EditorWindow.GetWindow(typeof(Preferences));
window.titleContent = new GUIContent(ClonesManager.ProjectName + " Preferences");
window.minSize = new Vector2(550, 300);
window.Show();
}
/// <summary>
/// Disable asset saving in clone editors?
/// </summary>
public static BoolPreference AssetModPref = new BoolPreference("ParrelSync_DisableClonesAssetSaving", true);
/// <summary>
/// In addition of checking the existence of UnityLockFile,
/// also check is the is the UnityLockFile being opened.
/// </summary>
public static BoolPreference AlsoCheckUnityLockFileStaPref = new BoolPreference("ParrelSync_CheckUnityLockFileOpenStatus", true);
/// <summary>
/// A list of folders to create sybolic links for,
/// useful for data that lives outside of the assets folder
/// eg. Wwise project data
/// </summary>
public static ListOfStringsPreference OptionalSymbolicLinkFolders = new ListOfStringsPreference("ParrelSync_OptionalSymbolicLinkFolders");
private void OnGUI()
{
if (ClonesManager.IsClone())
{
EditorGUILayout.HelpBox(
"This is a clone project. Please use the original project editor to change preferences.",
MessageType.Info);
return;
}
GUILayout.BeginVertical("HelpBox");
GUILayout.Label("Preferences");
GUILayout.BeginVertical("GroupBox");
AssetModPref.Value = EditorGUILayout.ToggleLeft(
new GUIContent(
"(recommended) Disable asset saving in clone editors- require re-open clone editors",
"Disable asset saving in clone editors so all assets can only be modified from the original project editor"
),
AssetModPref.Value);
if (Application.platform == RuntimePlatform.WindowsEditor)
{
AlsoCheckUnityLockFileStaPref.Value = EditorGUILayout.ToggleLeft(
new GUIContent(
"Also check UnityLockFile lock status while checking clone projects running status",
"Disable this can slightly increase Clones Manager window performance, but will lead to in-correct clone project running status" +
"(the Clones Manager window show the clone project is still running even it's not) if the clone editor crashed"
),
AlsoCheckUnityLockFileStaPref.Value);
}
GUILayout.EndVertical();
GUILayout.BeginVertical("GroupBox");
GUILayout.Label("Optional Folders to Symbolically Link");
GUILayout.Space(5);
// cache the current value
List<string> optionalFolderPaths = OptionalSymbolicLinkFolders.GetStoredValue();
bool optionalFolderPathsAreDirty = false;
// append a new row if full
if (optionalFolderPaths.Last() != "")
{
optionalFolderPaths.Add("");
}
var projectPath = ClonesManager.GetCurrentProjectPath();
for (int i = 0; i < optionalFolderPaths.Count; ++i)
{
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField(optionalFolderPaths[i], EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select Folder", GUILayout.Width(100)))
{
var result = EditorUtility.OpenFolderPanel("Select Folder to Symbolically Link...", "", "");
if (result.Contains(projectPath))
{
optionalFolderPaths[i] = result.Replace(projectPath,"");
optionalFolderPathsAreDirty = true;
}
else if( result != "")
{
Debug.LogWarning("Symbolic Link folder must be within the project directory");
}
}
if (GUILayout.Button("Clear", GUILayout.Width(100)))
{
optionalFolderPaths[i] = "";
optionalFolderPathsAreDirty = true;
}
GUILayout.EndHorizontal();
}
// only set the preference if the value is marked dirty
if (optionalFolderPathsAreDirty)
{
optionalFolderPaths.RemoveAll(str=> str == "");
OptionalSymbolicLinkFolders.SetStoredValue(optionalFolderPaths);
}
GUILayout.EndVertical();
if (GUILayout.Button("Reset to default"))
{
AssetModPref.ClearValue();
AlsoCheckUnityLockFileStaPref.ClearValue();
OptionalSymbolicLinkFolders.ClearStoredValue();
Debug.Log("Editor preferences cleared");
}
GUILayout.EndVertical();
}
}
}

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

@ -0,0 +1,112 @@
using System.Collections.Generic;
using System.Linq;
namespace ParrelSync
{
public class Project : System.ICloneable
{
public string name;
public string projectPath;
string rootPath;
public string assetPath;
public string projectSettingsPath;
public string libraryPath;
public string packagesPath;
public string autoBuildPath;
public string localPackages;
char[] separator = new char[1] { '/' };
/// <summary>
/// Default constructor
/// </summary>
public Project()
{
}
/// <summary>
/// Initialize the project object by parsing its full path returned by Unity into a bunch of individual folder names and paths.
/// </summary>
/// <param name="path"></param>
public Project(string path)
{
ParsePath(path);
}
/// <summary>
/// Create a new object with the same settings
/// </summary>
/// <returns></returns>
public object Clone()
{
Project newProject = new Project();
newProject.rootPath = rootPath;
newProject.projectPath = projectPath;
newProject.assetPath = assetPath;
newProject.projectSettingsPath = projectSettingsPath;
newProject.libraryPath = libraryPath;
newProject.name = name;
newProject.separator = separator;
newProject.packagesPath = packagesPath;
newProject.autoBuildPath = autoBuildPath;
newProject.localPackages = localPackages;
return newProject;
}
/// <summary>
/// Update the project object by renaming and reparsing it. Pass in the new name of a project, and it'll update the other member variables to match.
/// </summary>
/// <param name="name"></param>
public void updateNewName(string newName)
{
name = newName;
ParsePath(rootPath + "/" + name + "/Assets");
}
/// <summary>
/// Debug override so we can quickly print out the project info.
/// </summary>
/// <returns></returns>
public override string ToString()
{
string printString = name + "\n" +
rootPath + "\n" +
projectPath + "\n" +
assetPath + "\n" +
projectSettingsPath + "\n" +
packagesPath + "\n" +
autoBuildPath + "\n" +
localPackages + "\n" +
libraryPath;
return (printString);
}
private void ParsePath(string path)
{
//Unity's Application functions return the Assets path in the Editor.
projectPath = path;
//pop off the last part of the path for the project name, keep the rest for the root path
List<string> pathArray = projectPath.Split(separator).ToList<string>();
name = pathArray.Last();
pathArray.RemoveAt(pathArray.Count() - 1);
rootPath = string.Join(separator[0].ToString(), pathArray.ToArray());
assetPath = projectPath + "/Assets";
projectSettingsPath = projectPath + "/ProjectSettings";
libraryPath = projectPath + "/Library";
packagesPath = projectPath + "/Packages";
autoBuildPath = projectPath + "/AutoBuild";
localPackages = projectPath + "/LocalPackages";
}
}
}

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

@ -0,0 +1,60 @@
using System;
using UnityEditor;
using UnityEngine;
namespace ParrelSync.Update
{
/// <summary>
/// A simple update checker
/// </summary>
public class UpdateChecker
{
//const string LocalVersionFilePath = "Assets/ParrelSync/VERSION.txt";
public const string LocalVersion = "1.5.2";
[MenuItem("ParrelSync/Check for update", priority = 20)]
static void CheckForUpdate()
{
using (System.Net.WebClient client = new System.Net.WebClient())
{
try
{
//This won't work with UPM packages
//string localVersionText = AssetDatabase.LoadAssetAtPath<TextAsset>(LocalVersionFilePath).text;
string localVersionText = LocalVersion;
Debug.Log("Local version text : " + LocalVersion);
string latesteVersionText = client.DownloadString(ExternalLinks.RemoteVersionURL);
Debug.Log("latest version text got: " + latesteVersionText);
string messageBody = "Current Version: " + localVersionText +"\n"
+"Latest Version: " + latesteVersionText + "\n";
var latestVersion = new Version(latesteVersionText);
var localVersion = new Version(localVersionText);
if (latestVersion > localVersion)
{
Debug.Log("There's a newer version");
messageBody += "There's a newer version available";
if(EditorUtility.DisplayDialog("Check for update.", messageBody, "Get latest release", "Close"))
{
Application.OpenURL(ExternalLinks.Releases);
}
}
else
{
Debug.Log("Current version is up-to-date.");
messageBody += "Current version is up-to-date.";
EditorUtility.DisplayDialog("Check for update.", messageBody,"OK");
}
}
catch (Exception exp)
{
Debug.LogError("Error with checking update. Exception: " + exp);
EditorUtility.DisplayDialog("Update Error","Error with checking update. \nSee console for more details.",
"OK"
);
}
}
}
}
}

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

@ -0,0 +1,73 @@
namespace ParrelSync
{
using UnityEditor;
using UnityEngine;
using System;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Linq;
[InitializeOnLoad]
public class ValidateCopiedFoldersIntegrity
{
const string SessionStateKey = "ValidateCopiedFoldersIntegrity_Init";
/// <summary>
/// Called once on editor startup.
/// Validate copied folders integrity in clone project
/// </summary>
static ValidateCopiedFoldersIntegrity()
{
if (!SessionState.GetBool(SessionStateKey, false))
{
SessionState.SetBool(SessionStateKey, true);
if (!ClonesManager.IsClone()) { return; }
ValidateFolder(ClonesManager.GetCurrentProjectPath(), ClonesManager.GetOriginalProjectPath(), "Packages");
}
}
public static void ValidateFolder(string targetRoot, string originalRoot, string folderName)
{
var targetFolderPath = Path.Combine(targetRoot, folderName);
var targetFolderHash = CreateMd5ForFolder(targetFolderPath);
var originalFolderPath = Path.Combine(originalRoot, folderName);
var originalFolderHash = CreateMd5ForFolder(originalFolderPath);
if (targetFolderHash != originalFolderHash)
{
Debug.Log("ParrelSync: Detected changes in '" + folderName + "' directory. Updating cloned project...");
FileUtil.ReplaceDirectory(originalFolderPath, targetFolderPath);
}
}
static string CreateMd5ForFolder(string path)
{
// assuming you want to include nested folders
var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories)
.OrderBy(p => p).ToList();
MD5 md5 = MD5.Create();
for (int i = 0; i < files.Count; i++)
{
string file = files[i];
// hash path
string relativePath = file.Substring(path.Length + 1);
byte[] pathBytes = Encoding.UTF8.GetBytes(relativePath.ToLower());
md5.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0);
// hash contents
byte[] contentBytes = File.ReadAllBytes(file);
if (i == files.Count - 1)
md5.TransformFinalBlock(contentBytes, 0, contentBytes.Length);
else
md5.TransformBlock(contentBytes, 0, contentBytes.Length, contentBytes, 0);
}
return BitConverter.ToString(md5.Hash).Replace("-", "").ToLower();
}
}
}

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

@ -0,0 +1,10 @@
{
"name": "com.veriorpies.parrelsync",
"displayName": "ParrelSync",
"version": "1.5.2",
"unity": "2018.4",
"description": "ParrelSync is a Unity editor extension that allows users to test multiplayer gameplay without building the project by having another Unity editor window opened and mirror the changes from the original project.",
"license": "MIT",
"keywords": [ "Networking", "Utils", "Editor", "Extensions" ],
"dependencies": {}
}

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

@ -0,0 +1,15 @@
{
"name": "ParrelSync",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 894a6cc6ed5cd2645bb542978cbed6a9
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

@ -7,7 +7,7 @@ PhysicMaterial:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Friction
dynamicFriction: 0.069981694
dynamicFriction: 0.069989614
staticFriction: 1
bounciness: 0.3
frictionCombine: 3

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 26ebab9e6ff8cab45a17727794745030
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 593db82065bbf0a41b5c96f2bdf6f1af
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 051f4ac7fc46ef748b48a56432be90a9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

@ -0,0 +1,15 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c0011418c9d75434988a06b6df93b283, type: 3}
m_Name: ParrelSyncProjectSettings
m_EditorClassIdentifier:
m_OptionalSymbolicLinkFolders: []

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 762e6ea03aa818f43a3a0c1b6b089dc3
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

@ -6646,14 +6646,12 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: a7354a95ac207024894f426c22c2b811, type: 3}
m_Name:
m_EditorClassIdentifier:
_currentNOS: 0
_chargeSpeed: 12
_depletionSpeed: 13.5
_boostAcceleration: 40
_audioVolume: 0.5
_boostAudio: {fileID: 0}
_isBoosting: 0
_boostBarFill: {fileID: 0}
_CurrentNOS: 0
chargeSpeed: 1
depletionSpeed: 1
boostAcceleration: 40
audioVolume: 0.5
boostAudio: {fileID: 0}
--- !u!1 &3067970480224447241
GameObject:
m_ObjectHideFlags: 0
@ -7954,6 +7952,7 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 4431542291951001293}
- component: {fileID: 3020599757856457735}
m_Layer: 0
m_Name: Player
m_TagString: Untagged
@ -7978,6 +7977,27 @@ Transform:
- {fileID: 1545892790134000353}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0}
--- !u!114 &3020599757856457735
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5097956068855214732}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -1552182283, guid: e725a070cec140c4caffb81624c8c787, type: 3}
m_Name:
m_EditorClassIdentifier:
SortKey: 4082538716
ObjectInterest: 1
Flags: 262145
NestedObjects: []
NetworkedBehaviours:
- {fileID: 9134473496284358468}
- {fileID: 8529057636489690899}
- {fileID: 2057786069090330772}
ForceRemoteRenderTimeframe: 0
--- !u!1 &5143393733947949188
GameObject:
m_ObjectHideFlags: 0

@ -1,5 +1,7 @@
fileFormatVersion: 2
guid: b7ef0b21caef7df4fbaf32ff73da212f
labels:
- FusionPrefab
PrefabImporter:
externalObjects: {}
userData:

@ -0,0 +1,68 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &7139577379577549923
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1742707189475752604}
- component: {fileID: 7284567899420768869}
- component: {fileID: 2434842119018639102}
m_Layer: 0
m_Name: RaceCountDownManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1742707189475752604
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7139577379577549923}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 540, y: 550, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &7284567899420768869
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7139577379577549923}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ce0876da7a902494595dadf7967ec99b, type: 3}
m_Name:
m_EditorClassIdentifier:
countdownText: {fileID: 0}
_RaceStarted: 0
--- !u!114 &2434842119018639102
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7139577379577549923}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -1552182283, guid: e725a070cec140c4caffb81624c8c787, type: 3}
m_Name:
m_EditorClassIdentifier:
SortKey: 130762371
ObjectInterest: 1
Flags: 262145
NestedObjects: []
NetworkedBehaviours:
- {fileID: 7284567899420768869}
ForceRemoteRenderTimeframe: 0

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 2595ebf73aeb93140a9aa786b6f4b970
labels:
- FusionPrefab
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

@ -0,0 +1,495 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1120236924525325949
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4314811329568637152}
- component: {fileID: 5409392243021292236}
- component: {fileID: 651076721910011157}
- component: {fileID: 6997821354237460575}
m_Layer: 5
m_Name: JoinButton
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4314811329568637152
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1120236924525325949}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2501889218278975373}
m_Father: {fileID: 3668090635799086485}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 0.5}
m_AnchorMax: {x: 1, y: 0.5}
m_AnchoredPosition: {x: -40, y: 0}
m_SizeDelta: {x: 160, y: 50}
m_Pivot: {x: 1, y: 0.5}
--- !u!222 &5409392243021292236
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1120236924525325949}
m_CullTransparentMesh: 1
--- !u!114 &651076721910011157
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1120236924525325949}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &6997821354237460575
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1120236924525325949}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 651076721910011157}
m_OnClick:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 274701417174442539}
m_TargetAssemblyTypeName: SessionInfoUIListItem, Assembly-CSharp
m_MethodName: OnClick
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!1 &2618561466982086480
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6164581029060872703}
- component: {fileID: 5292430734714898303}
- component: {fileID: 5141517220623329614}
m_Layer: 5
m_Name: SessionNameDummy
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6164581029060872703
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2618561466982086480}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3668090635799086485}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0.5}
m_AnchorMax: {x: 0, y: 0.5}
m_AnchoredPosition: {x: 40, y: 0}
m_SizeDelta: {x: 500, y: 50}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &5292430734714898303
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2618561466982086480}
m_CullTransparentMesh: 1
--- !u!114 &5141517220623329614
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2618561466982086480}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Session Name
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 44.75
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &6127582303056407658
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2501889218278975373}
- component: {fileID: 1416419335608080642}
- component: {fileID: 8558196728551479886}
m_Layer: 5
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2501889218278975373
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6127582303056407658}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4314811329568637152}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1416419335608080642
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6127582303056407658}
m_CullTransparentMesh: 1
--- !u!114 &8558196728551479886
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6127582303056407658}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: JOIN
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 44.75
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &7569151483051384423
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3668090635799086485}
- component: {fileID: 522063748126281104}
- component: {fileID: 1054626397041665074}
- component: {fileID: 274701417174442539}
m_Layer: 5
m_Name: SessionTitle
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3668090635799086485
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7569151483051384423}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6164581029060872703}
- {fileID: 4314811329568637152}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 960, y: 100}
m_Pivot: {x: 0.5, y: 1}
--- !u!222 &522063748126281104
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7569151483051384423}
m_CullTransparentMesh: 1
--- !u!114 &1054626397041665074
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7569151483051384423}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.4134194, g: 0.3820755, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 0}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &274701417174442539
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7569151483051384423}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 91a3ffeea0eaa1b43adc497d41d36471, type: 3}
m_Name:
m_EditorClassIdentifier:
sessionName: {fileID: 5141517220623329614}
joinButton: {fileID: 6997821354237460575}

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

@ -0,0 +1,59 @@
using UnityEngine;
using TMPro;
using Fusion;
using System.Collections;
public class RaceCountdownManager : NetworkBehaviour
{
public static RaceCountdownManager Instance;
[SerializeField] private TextMeshProUGUI countdownText;
[Networked, OnChangedRender(nameof(OnCountdownChanged))]
private int CountdownTimer { get; set; }
[Networked] public bool RaceStarted { get; private set; }
private void Awake()
{
Instance = this;
if (countdownText != null)
countdownText.gameObject.SetActive(false);
}
public override void Spawned()
{
if (Object.HasStateAuthority)
{
StartCoroutine(CountdownCoroutine());
}
}
private IEnumerator CountdownCoroutine()
{
CountdownTimer = 3;
countdownText.gameObject.SetActive(true);
while (CountdownTimer > 0)
{
yield return new WaitForSeconds(1f);
CountdownTimer--;
}
RaceStarted = true;
countdownText.gameObject.SetActive(false);
}
private void OnCountdownChanged()
{
if (CountdownTimer > 0)
{
countdownText.text = CountdownTimer.ToString();
countdownText.gameObject.SetActive(true);
}
else
{
countdownText.gameObject.SetActive(false);
}
}
}

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

@ -1065,6 +1065,7 @@ RectTransform:
- {fileID: 551422371}
- {fileID: 653252760}
- {fileID: 1906425622}
- {fileID: 1614665874}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@ -1145,6 +1146,13 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
runner: {fileID: 824699893}
lobbyUI: {fileID: 0}
sessionListUIHandler: {fileID: 0}
lobbyNameInput: {fileID: 0}
createLobbyButton: {fileID: 0}
waitingText: {fileID: 0}
playerPrefab:
RawGuidValue: 00000000000000000000000000000000
--- !u!114 &824699893
MonoBehaviour:
m_ObjectHideFlags: 0
@ -2402,6 +2410,187 @@ Transform:
m_Children: []
m_Father: {fileID: 2062866432}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1614665873
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1614665874}
- component: {fileID: 1614665876}
- component: {fileID: 1614665875}
m_Layer: 5
m_Name: RaceCountDownText
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1614665874
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1614665873}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 824599549}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1614665875
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1614665873}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text:
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 36
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 18
m_fontSizeMax: 118.09
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &1614665876
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1614665873}
m_CullTransparentMesh: 1
--- !u!1 &1627785074
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1627785076}
- component: {fileID: 1627785075}
m_Layer: 0
m_Name: SpawnPointProvider
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1627785075
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1627785074}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6ac4525b2170468478fadfba1f9b4f9e, type: 3}
m_Name:
m_EditorClassIdentifier:
spawnPoints:
- {fileID: 1868736289}
- {fileID: 1928440683}
--- !u!4 &1627785076
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1627785074}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -0.4686718, y: -4.3724966, z: -3.977809}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1632319060
GameObject:
m_ObjectHideFlags: 0
@ -2646,6 +2835,37 @@ Transform:
m_Children: []
m_Father: {fileID: 2062866432}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1868736288
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1868736289}
m_Layer: 0
m_Name: SpawnPoint1
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1868736289
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1868736288}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -48.340305, y: -17.914295, z: -5.1727653}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1897337258
GameObject:
m_ObjectHideFlags: 0
@ -3005,6 +3225,37 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1928138934}
m_CullTransparentMesh: 1
--- !u!1 &1928440682
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1928440683}
m_Layer: 0
m_Name: SpawnPoint2
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1928440683
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1928440682}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -48.340305, y: -17.914295, z: 1.85}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &2062866431
GameObject:
m_ObjectHideFlags: 0
@ -15320,6 +15571,75 @@ GameObject:
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!1001 &5227700218869196845
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 1742707189475752604, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_LocalPosition.x
value: 540
objectReference: {fileID: 0}
- target: {fileID: 1742707189475752604, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_LocalPosition.y
value: 550
objectReference: {fileID: 0}
- target: {fileID: 1742707189475752604, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1742707189475752604, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1742707189475752604, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1742707189475752604, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1742707189475752604, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1742707189475752604, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1742707189475752604, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1742707189475752604, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2434842119018639102, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: Flags
value: 262145
objectReference: {fileID: 0}
- target: {fileID: 2434842119018639102, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: SortKey
value: 2364241705
objectReference: {fileID: 0}
- target: {fileID: 7139577379577549923, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: m_Name
value: RaceCountDownManager
objectReference: {fileID: 0}
- target: {fileID: 7284567899420768869, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
propertyPath: countdownText
value:
objectReference: {fileID: 1614665875}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 2595ebf73aeb93140a9aa786b6f4b970, type: 3}
--- !u!23 &5247992814140866107
MeshRenderer:
m_ObjectHideFlags: 0
@ -23631,3 +23951,7 @@ SceneRoots:
- {fileID: 1897337261}
- {fileID: 824699890}
- {fileID: 2531209556464813522}
- {fileID: 5227700218869196845}
- {fileID: 1627785076}
- {fileID: 1868736289}
- {fileID: 1928440683}

File diff suppressed because it is too large Load Diff

@ -1,71 +1,86 @@
using UnityEngine;
[RequireComponent(typeof(Camera))]
public class CameraController : MonoBehaviour
{
[Header("Target")]
[SerializeField] Transform target;
[Tooltip("If set, camera will follow this Transform. Otherwise it will auto-find the local player.")]
[SerializeField] private Transform target;
[Header("Zoom Settings")]
[SerializeField] float baseDistance = 6f;
[SerializeField] float maxDistance = 12f;
[SerializeField] float zoomSpeed = 5f;
[SerializeField] private float baseDistance = 6f;
[SerializeField] private float maxDistance = 12f;
[SerializeField] private float zoomSpeed = 5f;
[Header("Follow Settings")]
[SerializeField] float followSmoothness = 5f;
[SerializeField] Vector3 heightOffset = new Vector3(0, 3f, 0); // optional vertical offset
[SerializeField] private float followSmoothness = 5f;
[SerializeField] private Vector3 heightOffset = new Vector3(0, 3f, 0);
private Rigidbody targetRb;
private float currentDistance;
public Camera mainCam;
private Camera cam;
private void Start()
{
Application.targetFrameRate = 120;
cam = GetComponent<Camera>();
currentDistance = baseDistance;
// Check if we are the local player
//if (!GetComponentInParent<NetworkIdentity>().isLocalPlayer)
//{
// if (cam != null) cam.enabled = false;
// gameObject.SetActive(false); // disable the entire camera object
// return;
//}
Application.targetFrameRate = 120;
if (target == null)
if (target != null)
{
Debug.LogError("CameraController: Target not assigned!");
enabled = false;
return;
// User assigned a target in the Inspector
InitializeTarget();
}
else
{
// No target assigned, try to auto-find after a short delay
Invoke(nameof(FindLocalPlayer), 0.5f);
}
}
private void InitializeTarget()
{
targetRb = target.GetComponent<Rigidbody>();
if (targetRb == null)
{
Debug.LogError("CameraController: Target must have a Rigidbody.");
Debug.LogError("CameraController: Assigned target has no Rigidbody component.", this);
enabled = false;
return;
}
mainCam = GetComponent<Camera>();
currentDistance = baseDistance;
Debug.Log("CameraController: Following manually assigned target.", this);
}
private void FindLocalPlayer()
{
foreach (var vehicle in FindObjectsOfType<VehicleController>())
{
// VehicleController inherits NetworkBehaviour, so .Object is the NetworkObject
if (vehicle.Object != null && vehicle.Object.HasInputAuthority)
{
target = vehicle.FollowTarget;
InitializeTarget();
Debug.Log("CameraController: Auto-found local player to follow.", this);
return;
}
}
Debug.LogWarning("CameraController: No local player found to follow — disabling.", this);
enabled = false;
}
private void LateUpdate()
{
float speed = targetRb.velocity.magnitude;
if (target == null || targetRb == null) return;
// Zoom out based on speed
float speed = targetRb.velocity.magnitude;
float targetDistance = Mathf.Lerp(baseDistance, maxDistance, speed / 30f);
currentDistance = Mathf.Lerp(currentDistance, targetDistance, Time.deltaTime * zoomSpeed);
// Calculate camera position: behind the target
Vector3 followPos = target.position + heightOffset - target.forward * currentDistance;
Vector3 followPos = target.position
+ heightOffset
- target.forward * currentDistance;
// Smoothly move camera
transform.position = Vector3.Lerp(transform.position, followPos, Time.deltaTime * followSmoothness);
// Look at the target (optional slight offset)
transform.LookAt(target.position + heightOffset * 0.5f);
}
}

@ -1,120 +1,312 @@
using Fusion;
using Fusion.Sockets;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
using UnityEngine.UI;
using System.Linq;
public class FusionLauncher : MonoBehaviour, INetworkRunnerCallbacks
{
[Header("Network Runner")]
public NetworkRunner runner;
[Header("Network Runner (Drag NOTHING here)")]
public NetworkRunner runner; // we'll create/destroy this at runtime
[Header("Lobby UI")]
public GameObject lobbyUI;
public Transform lobbyListContainer;
public GameObject lobbyButtonPrefab;
public SessionListUIHandler sessionListUIHandler;
public TMP_InputField lobbyNameInput;
public Button createLobbyButton;
public TextMeshProUGUI waitingText;
private void Start()
[Header("Player Prefab")]
public NetworkPrefabRef playerPrefab;
private Coroutine refreshCoroutine;
private int playerCount = 0;
private const int maxPlayers = 2;
private bool gameplayLoaded = false;
private bool connectedToServer = false;
void Awake()
{
// Ensure this launcher persists across loads
if (FindObjectsOfType<FusionLauncher>().Length > 1)
{
Destroy(gameObject);
return;
}
DontDestroyOnLoad(gameObject);
}
void Start()
{
// Kick off our dummy runner purely in client/browse mode:
StartDummyRunner();
// UI hookups
if (lobbyNameInput != null) lobbyNameInput.onValueChanged.AddListener(OnLobbyNameChanged);
OnLobbyNameChanged(lobbyNameInput.text);
waitingText.gameObject.SetActive(false);
// Start periodic lobby refresh once connected
refreshCoroutine = StartCoroutine(AutoRefreshLobbyList());
}
void OnDestroy()
{
if (refreshCoroutine != null)
StopCoroutine(refreshCoroutine);
}
IEnumerator AutoRefreshLobbyList()
{
if (runner == null)
while (true)
{
Debug.LogError("Runner is not assigned in the Inspector!");
RefreshLobbyList();
yield return new WaitForSeconds(5f);
}
}
void OnLobbyNameChanged(string s)
{
createLobbyButton.interactable = !string.IsNullOrWhiteSpace(s) && s.Length <= 15;
}
// —————— DUMMY RUNNER ——————
// Starts a runner in CLIENT mode so we can list sessions.
async void StartDummyRunner()
{
// If already up & running, just bail.
if (runner != null && runner.IsRunning)
return;
// Ensure any old runner is cleared
await EnsureFreshRunner();
// Start as CLIENT
var result = await runner.StartGame(new StartGameArgs()
{
GameMode = GameMode.Client,
SceneManager = runner.gameObject.AddComponent<NetworkSceneManagerDefault>()
});
if (result.Ok)
{
Debug.Log("✅ Browser runner connected (Client mode).");
connectedToServer = true;
}
else if (result.ShutdownReason == ShutdownReason.GameNotFound)
{
Debug.Log(" No lobbies yet—but browser runner is live.");
connectedToServer = true;
}
else
{
Debug.LogError($"❌ Browser runner failed: {result.ShutdownReason}");
}
}
// —————— RESET / CREATE FRESH RUNNER ——————
private async Task EnsureFreshRunner()
{
// Reset flags
playerCount = 0;
gameplayLoaded = false;
connectedToServer = false;
// If an old runner exists & is running, shut it down & destroy its GO
if (runner != null && runner.IsRunning)
{
await runner.Shutdown();
Destroy(runner.gameObject);
}
// Stop auto-refresh while we rebuild
if (refreshCoroutine != null)
{
StopCoroutine(refreshCoroutine);
refreshCoroutine = null;
}
// Make brand-new runner GameObject
var go = new GameObject("NetworkRunnerGO");
DontDestroyOnLoad(go); // keep it alive across scenes
runner = go.AddComponent<NetworkRunner>();
runner.ProvideInput = true;
runner.AddCallbacks(this);
}
// —————— CREATE LOBBY (Host) ——————
public async void CreateLobby()
{
string sessionName = "Lobby_" + Random.Range(1000, 9999);
var name = lobbyNameInput.text.Trim();
if (string.IsNullOrEmpty(name))
{
Debug.LogWarning("Empty lobby name!");
return;
}
var result = await runner.StartGame(new StartGameArgs()
// Tear down browse runner & spawn a fresh one
await EnsureFreshRunner();
var sceneRef = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex);
var res = await runner.StartGame(new StartGameArgs()
{
GameMode = GameMode.Host,
SessionName = sessionName,
PlayerCount = 2,
Scene = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex),
SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
SessionName = name,
PlayerCount = maxPlayers,
Scene = sceneRef,
SceneManager = runner.gameObject.AddComponent<NetworkSceneManagerDefault>()
});
if (result.Ok)
if (res.Ok)
{
Debug.Log($"✔ Created Lobby: {sessionName}");
Debug.Log($"✔ Hosted Lobby '{name}'");
lobbyUI.SetActive(false);
waitingText.text = "Waiting for player 2…";
waitingText.gameObject.SetActive(true);
}
else
{
Debug.LogError($"✖ Failed to create lobby: {result.ShutdownReason}");
Debug.LogError($"✖ Host failed: {res.ShutdownReason}");
}
}
public async void JoinLobby(string lobbyName)
// —————— JOIN LOBBY (Client) ——————
public async void JoinLobby(SessionInfo info)
{
var result = await runner.StartGame(new StartGameArgs()
// Tear down browse runner & spawn a fresh one
await EnsureFreshRunner();
var res = await runner.StartGame(new StartGameArgs()
{
GameMode = GameMode.Client,
SessionName = lobbyName,
SessionName = info.Name,
Scene = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex),
SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
SceneManager = runner.gameObject.AddComponent<NetworkSceneManagerDefault>()
});
if (result.Ok)
if (res.Ok)
{
Debug.Log($"✔ Joined Lobby: {lobbyName}");
Debug.Log($"✔ Joined Lobby '{info.Name}'");
lobbyUI.SetActive(false);
waitingText.text = "Waiting for host…";
waitingText.gameObject.SetActive(true);
}
else
{
Debug.LogError($"✖ Failed to join lobby: {result.ShutdownReason}");
Debug.LogError($"✖ Join failed: {res.ShutdownReason}");
}
}
// —————— REFRESH LOBBY LIST ——————
public void RefreshLobbyList()
{
runner.JoinSessionLobby(SessionLobby.ClientServer); // This triggers OnSessionListUpdated
if (!connectedToServer) return;
//sessionListUIHandler.ClearList();
runner.JoinSessionLobby(SessionLobby.ClientServer);
}
// —————— FUSION CALLBACKS ——————
public void OnConnectedToServer(NetworkRunner r)
{
connectedToServer = true;
Debug.Log("✔ Connected to server; can refresh lobbies.");
RefreshLobbyList();
}
public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList)
{
foreach (Transform child in lobbyListContainer)
// First thing, wipe out the old items every time
sessionListUIHandler.ClearList();
// If there really are no sessions, show your “no sessions” message
if (sessionList.Count == 0)
{
Destroy(child.gameObject);
sessionListUIHandler.ShowNoSessionsMessage();
return;
}
// Otherwise re-populate
foreach (var session in sessionList)
{
if (session.IsOpen && session.IsVisible && session.PlayerCount < session.MaxPlayers)
{
GameObject button = Instantiate(lobbyButtonPrefab, lobbyListContainer);
button.GetComponentInChildren<Text>().text = session.Name;
button.GetComponent<Button>().onClick.AddListener(() => JoinLobby(session.Name));
sessionListUIHandler.AddToList(session, JoinLobby);
}
}
}
#region INetworkRunnerCallbacks (Empty implementations)
public void OnConnectedToServer(NetworkRunner runner) { }
public void OnDisconnectedFromServer(NetworkRunner runner, NetDisconnectReason reason) { }
public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token) { }
public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason) { }
public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { }
public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason) { }
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) { }
public void OnPlayerLeft(NetworkRunner runner, PlayerRef player) { }
public void OnInput(NetworkRunner runner, NetworkInput input) { }
public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { }
public void OnObjectEnterAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { }
public void OnObjectExitAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { }
public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key, System.ArraySegment<byte> data) { }
public void OnReliableDataProgress(NetworkRunner runner, PlayerRef player, ReliableKey key, float progress) { }
public void OnSceneLoadDone(NetworkRunner runner) { }
public void OnSceneLoadStart(NetworkRunner runner) { }
public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) { }
public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { }
#endregion
public void OnPlayerJoined(NetworkRunner r, PlayerRef p)
{
playerCount++;
// Only the host in Gameplay scene spawns cars
//if (r.IsServer && SceneManager.GetActiveScene().name == "Gameplay")
//{
// var pos = new Vector3(Random.Range(-5, 5), 0, Random.Range(-5, 5));
// r.Spawn(playerPrefab, pos, Quaternion.identity, p);
//}
// Only the host triggers sceneload when 2 players are present
if (r.IsServer && playerCount == maxPlayers && !gameplayLoaded)
{
gameplayLoaded = true;
var scene = SceneRef.FromIndex(
SceneUtility.GetBuildIndexByScenePath("Assets/Scenes/Gameplay.unity")
);
r.LoadScene(scene, LoadSceneMode.Single);
waitingText.gameObject.SetActive(false);
}
}
public void OnSceneLoadDone(NetworkRunner runner)
{
if (!runner.IsServer) return;
// only run once per game start
if (gameplayLoaded)
{
gameplayLoaded = false;
// grab your provider
var provider = FindObjectOfType<SpawnPointProvider>();
if (provider == null || provider.spawnPoints == null || provider.spawnPoints.Length == 0)
{
Debug.LogError("No SpawnPointProvider or no spawnPoints assigned!");
return;
}
// convert the IEnumerable to an array
PlayerRef[] players = runner.ActivePlayers.ToArray();
// pick the smaller of (number of players) vs (number of spawnPoints)
int count = Mathf.Min(players.Length, provider.spawnPoints.Length);
for (int i = 0; i < count; i++)
{
PlayerRef p = players[i];
Transform sp = provider.spawnPoints[i];
runner.Spawn(playerPrefab, sp.position, sp.rotation, p);
}
}
}
public void OnDisconnectedFromServer(NetworkRunner r, NetDisconnectReason reason) { }
public void OnConnectRequest(NetworkRunner r, NetworkRunnerCallbackArgs.ConnectRequest req, byte[] tok) { }
public void OnConnectFailed(NetworkRunner r, NetAddress addr, NetConnectFailedReason reason) { }
public void OnUserSimulationMessage(NetworkRunner r, SimulationMessagePtr msg) { }
public void OnShutdown(NetworkRunner r, ShutdownReason reason) { }
public void OnPlayerLeft(NetworkRunner r, PlayerRef p) { }
public void OnInput(NetworkRunner r, NetworkInput input) { }
public void OnInputMissing(NetworkRunner r, PlayerRef p, NetworkInput input) { }
public void OnObjectEnterAOI(NetworkRunner r, NetworkObject obj, PlayerRef p) { }
public void OnObjectExitAOI(NetworkRunner r, NetworkObject obj, PlayerRef p) { }
public void OnReliableDataReceived(NetworkRunner r, PlayerRef p, ReliableKey k, System.ArraySegment<byte> data) { }
public void OnReliableDataProgress(NetworkRunner r, PlayerRef p, ReliableKey k, float prog) { }
//public void OnSceneLoadDone(NetworkRunner r) { }
public void OnSceneLoadStart(NetworkRunner r) { }
public void OnHostMigration(NetworkRunner r, HostMigrationToken t) { }
public void OnCustomAuthenticationResponse(NetworkRunner r, Dictionary<string, object> data) { }
}

@ -0,0 +1,40 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Fusion;
public class SessionInfoUIListItem : MonoBehaviour
{
public TextMeshProUGUI sessionName;
public Button joinButton;
private SessionInfo sessionInfo;
public event Action<SessionInfo> OnJoinSession;
public void SetSessionInfo(SessionInfo sessionInfo)
{
if (sessionName == null)
{
Debug.LogError($"[SessionInfoUIListItem] sessionName is null on '{name}'. Please assign a TextMeshProUGUI.");
return;
}
if (joinButton == null)
{
Debug.LogError($"[SessionInfoUIListItem] joinButton is null on '{name}'. Please assign a Button.");
return;
}
this.sessionInfo = sessionInfo;
sessionName.text = sessionInfo.Name;
joinButton.interactable = sessionInfo.PlayerCount < sessionInfo.MaxPlayers;
joinButton.onClick.RemoveAllListeners();
joinButton.onClick.AddListener(OnClick);
}
private void OnClick()
{
OnJoinSession?.Invoke(sessionInfo);
}
}

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

@ -94,29 +94,24 @@ public class VehicleController : NetworkBehaviour
InitializeVehicle();
}
// 1) Fusion will call this every tick to collect local input
// 2) Fusion runs your simulation here instead of Update/FixedUpdate
public override void FixedUpdateNetwork()
{
// First, only run simulation if the vehicle is “running”
if (!isRunning)
if (!isRunning || !RaceCountdownManager.Instance || !RaceCountdownManager.Instance.RaceStarted)
return;
// Try to fetch the fused input struct…
if (GetInput(out CarNetworkInput data))
if (Object.HasInputAuthority && GetInput(out CarNetworkInput data))
{
// …and only then unpack it
if (Object.HasInputAuthority)
{
inputHorizontal = data.Horizontal;
inputVertical = data.Vertical;
if (data.NOS)
ActivateNOS();
}
inputHorizontal = data.Horizontal;
inputVertical = data.Vertical;
if (data.NOS)
ActivateNOS();
}
// Now do your physics & visuals exactly as before
CheckGround();
HandleMovement();
UpdateVisuals();

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Fusion;
public class SessionListUIHandler : MonoBehaviour
{
public TextMeshProUGUI statusText;
public GameObject sessionItemListPrefab;
public VerticalLayoutGroup verticalLayoutGroup;
private List<(SessionInfoUIListItem item, Action<SessionInfo> handler)> activeItems = new();
public void ClearList()
{
foreach (var (item, handler) in activeItems)
{
item.OnJoinSession -= handler; // ✅ Correct way to unsubscribe
Destroy(item.gameObject);
}
activeItems.Clear();
statusText.gameObject.SetActive(false);
}
public void AddToList(SessionInfo sessionInfo, Action<SessionInfo> onJoinCallback)
{
var item = Instantiate(sessionItemListPrefab, verticalLayoutGroup.transform)
.GetComponent<SessionInfoUIListItem>();
item.SetSessionInfo(sessionInfo);
item.OnJoinSession += onJoinCallback;
activeItems.Add((item, onJoinCallback)); // store pair for clean removal
}
public void ShowNoSessionsMessage(string message = "No Game Sessions Found")
{
statusText.text = message;
statusText.gameObject.SetActive(true);
}
}

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

@ -0,0 +1,7 @@
using UnityEngine;
public class SpawnPointProvider : MonoBehaviour
{
[Tooltip("Assign your starting positions here in order: Player 1, Player 2, …")]
public Transform[] spawnPoints;
}

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

@ -1,5 +1,5 @@
using UnityEngine.EventSystems;
using UnityEngine;
using UnityEngine;
using UnityEngine.EventSystems;
namespace CnControls
{
@ -7,20 +7,24 @@ namespace CnControls
{
public AnimationCurve SensitivityCurve = new AnimationCurve(
new Keyframe(0f, 0f, 1f, 1f),
new Keyframe(1f, 1f, 1f, 1f));
new Keyframe(1f, 1f, 1f, 1f)
);
public override void OnDrag(PointerEventData eventData)
{
base.OnDrag(eventData);
var linearHorizontalValue = HorizintalAxis.Value;
var linearVecticalValue = VerticalAxis.Value;
// Read the raw values
float linearHorizontalValue = HorizontalAxis.Value;
float linearVerticalValue = VerticalAxis.Value;
var horizontalSign = Mathf.Sign(linearHorizontalValue);
var verticalSign = Mathf.Sign(linearVecticalValue);
// Keep the sign for correct direction
float horizontalSign = Mathf.Sign(linearHorizontalValue);
float verticalSign = Mathf.Sign(linearVerticalValue);
HorizintalAxis.Value = horizontalSign * SensitivityCurve.Evaluate(horizontalSign * linearHorizontalValue);
VerticalAxis.Value = verticalSign * SensitivityCurve.Evaluate(verticalSign * linearVecticalValue);
// Remap via the curve
HorizontalAxis.Value = horizontalSign * SensitivityCurve.Evaluate(horizontalSign * linearHorizontalValue);
VerticalAxis.Value = verticalSign * SensitivityCurve.Evaluate(verticalSign * linearVerticalValue);
}
}
}
}

@ -96,7 +96,7 @@ namespace CnControls
private float _oneOverMovementRange;
protected VirtualAxis HorizintalAxis;
protected VirtualAxis HorizontalAxis;
protected VirtualAxis VerticalAxis;
private void Awake()
@ -123,11 +123,11 @@ namespace CnControls
{
// When we enable, we get our virtual axis
HorizintalAxis = HorizintalAxis ?? new VirtualAxis(HorizontalAxisName);
HorizontalAxis = HorizontalAxis ?? new VirtualAxis(HorizontalAxisName);
VerticalAxis = VerticalAxis ?? new VirtualAxis(VerticalAxisName);
// And register them in our input system
CnInputManager.RegisterVirtualAxis(HorizintalAxis);
CnInputManager.RegisterVirtualAxis(HorizontalAxis);
CnInputManager.RegisterVirtualAxis(VerticalAxis);
}
@ -135,7 +135,7 @@ namespace CnControls
{
// When we disable, we just unregister our axis
// It also happens before the game object is Destroyed
CnInputManager.UnregisterVirtualAxis(HorizintalAxis);
CnInputManager.UnregisterVirtualAxis(HorizontalAxis);
CnInputManager.UnregisterVirtualAxis(VerticalAxis);
}
@ -204,7 +204,7 @@ namespace CnControls
//var verticalValue = Mathf.Clamp(finalDifference.y * _oneOverMovementRange, -1f, 1f);
// Finally, we update our virtual axis
HorizintalAxis.Value = horizontalValue;
HorizontalAxis.Value = horizontalValue;
VerticalAxis.Value = verticalValue;
}
@ -214,7 +214,7 @@ namespace CnControls
_stickTransform.anchoredPosition = _initialStickPosition;
_intermediateStickPosition = _initialStickPosition;
HorizintalAxis.Value = 0f;
HorizontalAxis.Value = 0f;
VerticalAxis.Value = 0f;
if (HideOnRelease)

@ -2,20 +2,24 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2180264
Material:
serializedVersion: 6
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: LiberationSans SDF Material
m_Shader: {fileID: 4800000, guid: fe393ace9b354375a9cb14cdbbc28be4, type: 3}
m_ShaderKeywords:
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 1
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
@ -67,6 +71,7 @@ Material:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Ambient: 0.5
- _Bevel: 0.5
@ -107,9 +112,9 @@ Material:
- _Parallax: 0.02
- _PerspectiveFilter: 0.875
- _Reflectivity: 10
- _ScaleRatioA: 0.90909094
- _ScaleRatioA: 0.9
- _ScaleRatioB: 0.73125
- _ScaleRatioC: 0.7386364
- _ScaleRatioC: 0.73125
- _ScaleX: 1
- _ScaleY: 1
- _ShaderFlags: 0
@ -148,6 +153,7 @@ Material:
- _ReflectOutlineColor: {r: 0, g: 0, b: 0, a: 1}
- _SpecularColor: {r: 1, g: 1, b: 1, a: 1}
- _UnderlayColor: {r: 0, g: 0, b: 0, a: 0.5}
m_BuildTextureStacks: []
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
@ -165,15 +171,16 @@ MonoBehaviour:
materialHashCode: 462855346
m_Version: 1.1.0
m_SourceFontFileGUID: e3265ab4bf004d28a9537516768c1c75
m_SourceFontFile_EditorRef: {fileID: 12800000, guid: e3265ab4bf004d28a9537516768c1c75,
type: 3}
m_SourceFontFile_EditorRef: {fileID: 12800000, guid: e3265ab4bf004d28a9537516768c1c75, type: 3}
m_SourceFontFile: {fileID: 12800000, guid: e3265ab4bf004d28a9537516768c1c75, type: 3}
m_AtlasPopulationMode: 1
m_FaceInfo:
m_FaceIndex: 0
m_FamilyName: Liberation Sans
m_StyleName: Regular
m_PointSize: 86
m_Scale: 1
m_UnitsPerEM: 0
m_LineHeight: 98.8916
m_AscentLine: 77.853516
m_CapLine: 59
@ -313,15 +320,21 @@ Texture2D:
Hash: 00000000000000000000000000000000
m_ForcedFallbackFormat: 4
m_DownscaleFallback: 0
m_IsAlphaChannelOptional: 0
serializedVersion: 2
m_Width: 0
m_Height: 0
m_CompleteImageSize: 0
m_MipsStripped: 0
m_TextureFormat: 1
m_MipCount: 1
m_IsReadable: 1
m_IsPreProcessed: 0
m_IgnoreMipmapLimit: 0
m_MipmapLimitGroupName:
m_StreamingMipmaps: 0
m_StreamingMipmapsPriority: 0
m_VTOnly: 0
m_AlphaIsTransparency: 0
m_ImageCount: 1
m_TextureDimension: 2
@ -335,9 +348,11 @@ Texture2D:
m_WrapW: 0
m_LightmapFormat: 0
m_ColorSpace: 0
m_PlatformBlob:
image data: 0
_typelessdata:
m_StreamData:
serializedVersion: 2
offset: 0
size: 0
path:

@ -5,6 +5,9 @@ EditorBuildSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Scenes:
- enabled: 1
path: Assets/Scenes/Menu.unity
guid: 5906ecf9ac800ca43864c639d1817021
- enabled: 1
path: Assets/Scenes/Gameplay.unity
guid: 287a16276b4a3ab43b035beb5ee1b07c

Loading…
Cancel
Save