Automatically add namespace to Unity C# script [duplicate] - c#

This question already has answers here:
Unity Add Default Namespace to Script Template?
(6 answers)
Closed 9 months ago.
I'm familiar with the ability to change script template for Unity game engine. However, it is very limited and only allows one keyword: #SCRIPTNAME#.
As project gets more and more complicated, namespaces becomes crucial part. And there's no way to have script generated with appropriate namespace through Create --> C# Script.
Does anyone know any solutions to this?
P.S. I am aware that you can create files with Visual Studio, that automatically get namespace based on location. However, they contain unnecessary parts like Assets.Scripts...

Doing some research online I've found that you can create AssetModificationProcessor with public static void OnWillCreateAsset(string path) method. In this method you can read created script file and replace content using string.Replace (or other methods).
Dwelling deeper into this, I've came with useful Editor script that changes #NAMESPACE# keyword based on script location in the project (all made using my current project structure, so you might need to adjust the script before it works correctly).
In case the link is broken, here's the editor script:
using System.IO;
using UnityEditor;
using UnityEngine;
namespace MyGame.Editor.Assets
{
public sealed class ScriptAssetKeywordsReplacer : UnityEditor.AssetModificationProcessor
{
/// <summary>
/// This gets called for every .meta file created by the Editor.
/// </summary>
public static void OnWillCreateAsset(string path)
{
path = path.Replace(".meta", string.Empty);
if (!path.EndsWith(".cs"))
{
return;
}
var systemPath = path.Insert(0, Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("Assets")));
ReplaceScriptKeywords(systemPath, path);
AssetDatabase.Refresh();
}
private static void ReplaceScriptKeywords(string systemPath, string projectPath)
{
projectPath = projectPath.Substring(projectPath.IndexOf("/SCRIPTS/") + "/SCRIPTS/".Length);
projectPath = projectPath.Substring(0, projectPath.LastIndexOf("/"));
projectPath = projectPath.Replace("/Scripts/", "/").Replace('/', '.');
var rootNamespace = string.IsNullOrWhiteSpace(EditorSettings.projectGenerationRootNamespace) ?
string.Empty :
$"{EditorSettings.projectGenerationRootNamespace}.";
var fullNamespace = $"{rootNamespace}{projectPath}";
var fileData = File.ReadAllText(systemPath);
fileData = fileData.Replace("#NAMESPACE#", fullNamespace);
File.WriteAllText(systemPath, fileData);
}
}
}
After adding this script under folder named Editor, you need to change c# script template which is located at %EditorPath%/Data/Resources/ScriptTemplates/81-C# Script-NewBehaviourScript.cs.txt. Open this file and add wrap class with
namespace #NAMESPACE#
{
// All the class template can stay the same
}

Related

Good practice for DeploymentItem?

I'm fairly rusty when it comes to C#. I've been poking my nose around internet trying to find a solution to my question without success.
I created a test project using MSTest. Some tests use files, that I added to my project test under the folder TestData, and they are copied when executing the test by using the attribute DeploymentItem.
Example: [DeploymentItem(#"TestData\test.txt")]
This copies test.txt at the execution folder and it works. However, when I want to use this file in the test, I then have to work on "test.txt" instead of #"TestData\test.txt". Thus, if I want to factorize my code, I have to have two variables:
const string testFileName = "test.txt";
const string testFilePath = #"TestData\test.txt";
and then use them as
[DeploymentItem(testFilePath)]
public void TestFunction()
{
[...]testFileName[...]
}
Ideally, I want instead to write:
[DeploymentItem(testFilePath)]
public void TestFunction()
{
[...]testFilePath[...]
}
This way I would only need one variable.
It would work if I use the second argument of DeploymentItem as such:
const string testFilesFolder = "TestData";
const string testFilePath = #"TestData\test.txt";
[DeploymentItem(testFilePath, testFilesFolder)]
public void TestFunction()
{
[...]testFilePath[...]
}
However, that forces me and everyone to think about passing the second argument every time we use DeploymentItem. But it has the merit of working.
Here are the different things I tried to do to address the issue:
Inheriting from DeploymentItem to simply add my own constructor: DeploymentItem is sealed so this is not possible.
Creating my own attribute, by copying the code of DeploymentItem. The file is not copied at all:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
class DeployFileAttribute : Attribute
{
public DeployFileAttribute(string path)
{
Path = path;
OutputDirectory = System.IO.Path.GetDirectoryName(path);
}
public string Path { get; }
public string OutputDirectory { get; }
}
[DeployFile(testFilePath)] // testFilePath is not copied at all, even though the constructor is correctly executed.
Creating a method that would return the attribute. It does not seem like it is possible to use the result of a method as an attribute:
public static DeploymentItemAttribute DeployFile(string path)
{
return new DeploymentItemAttribute(path, System.IO.Path.GetDirectoryName(path));
} // No compilation error
[DeployFile(testFilePath)] // DeployFileAttribute type does not exist
Creating something like a C++ style using statement or C style macro, I can't seem to find a syntax that works
using DeployFile(string toto) = DeploymentItemAttribute(toto, System.IO.Path.GetDirectoryName(path)); // Syntax is wrong, could not find one that works
Any hindsight would be welcome!
From my point of view, there are only two possibilities:
You use DeploymentItem in the way it was created by Microsoft.
[DeploymentItem(testFilePath, testFilesFolder)] as you manshioned in your post
You can combine source path:
const string testFileName = "test.txt";
[DeploymentItem(#"TestData\" + testFileName)]
public void TestFunction()
{
[...]testFileName[...]
}
In this case, you'll have just one variable :)
You can write your own extension for MSTest and create an attribute you need. But this is not the easy way. As key words for this approach, you could google for TestExtensionExecution, ITestMethodInvoker and TestClassExtensionAttribute
On the other hand, this is very understandable, why DeploymentItem is implemented as it is. Do not forget, that the source folder can be an absolute path as well. So assume, that you have the following attribute [DeploymentItem(#"S:\Shared\TestFiles\AAA\BBB\test.txt")] What should be the destination folder? But even with relative paths: [DeploymentItem(#"..\..\..\TestFiles\AAA\BBB\test.txt")] - can say the name of the destination folder in this case?

How to open a link on a new tab using WebGL/C# through Unity?

Currently opening a link through my game opens the link in the very same interface. I want it to open on a new tab. I tried researching plugins and whatnot so that a .jslib file interacts with the Unity project, but am having issues with that too. I'm new to this interaction so I'm also having problems finding the Plugin Inspector itself.
Currently, my code does this:
private void Update()
{
if (Input.GetKeyDown(KeyCode.Return))
{
Application.OpenURL("www.google.com");
}
}
So this opens the link on the same browser. I'm trying to make it so when the user hits the return key, they open that link on to a new browser.
Any help is appreciated!
One correct way is using .jslib file
Assets/plugins/plugin.jslib
var plugin = {
OpenNewTab : function(url)
{
url = Pointer_stringify(url);
window.open(url,'_blank');
},
};
mergeInto(LibraryManager.library, plugin);
Your C# script
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
public class OpenURL : MonoBehaviour
{
[DllImport("__Internal")]
private static extern void OpenNewTab(string url);
public void openIt(string url)
{
#if !UNITY_EDITOR && UNITY_WEBGL
OpenNewTab(url);
#endif
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Return))
{
openIt("www.wateverurluwant.com");
}
}
}
The current solution (since Unity 2019 or so) is the one Alienator5000 originally tried, with "Application.OpenURL".
It now opens your URL in a new tab. You can see more details on my response to this other stackoverflow post.
Have a nice day :)
Another solution in unity. You can still use ExternalEval, even if it is deprecated:
Application.ExternalEval("window.open('" + YourLink + "', '_blank')");

Unity Cloud Build: post export method

Problem:
I can't seem to figure out the right signature for Unity cloud build's post export method. According to the documentation:
The fully-qualified name of a public static method you want us to call
after we finish the Unity build process (but before Xcode). For
example: ClassName.CoolMethod or NameSpace.ClassName.CoolMethod. No
trailing parenthesis, and it can't have the same name as your
Pre-Export method! This method must accept a string parameter, which
will receive the path to the exported Unity player (or Xcode project
in the case of iOS).
Here is my code:
public static void OnPostprocessDevBuildIOS(string ExportPath)
{
var projPath = ExportPath + "/Unity-iPhone.xcodeproj/project.pbxproj";
var proj = new PBXProject();
var nativeTarget =
proj.TargetGuidByName(PBXProject.GetUnityTargetName());
var testTarget =
proj.TargetGuidByName(PBXProject.GetUnityTestTargetName());
string[] buildTargets = {nativeTarget, testTarget};
proj.ReadFromString(File.ReadAllText(projPath));
proj.SetBuildProperty(buildTargets, "ENABLE_BITCODE", "NO");
File.WriteAllText(projPath, proj.WriteToString());
}
and here is the error:
I've tried multiple test method signatures and can't seem to get anything to work. I've even tried just a method that logs out the path.
Additional Information:
Unity Version: 5.3.1f
Unity Cloud Build: 5.3.1f
Target: iOS 8.0+
Also, my cloud build settings script is located in the editor folder as required.
Ok so I got the the bitCode disabling post process to work with the following code, but only when I build manually. When I build from cloud build, with no error the app freezes at the splash screen. When I build from my local machine, the app runs just fine.
[PostProcessBuild]
public static void OnPostprocessBuild(BuildTarget buildTarget, string path)
{
if (buildTarget == BuildTarget.iOS)
{
string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
PBXProject proj = new PBXProject();
proj.ReadFromString(File.ReadAllText(projPath));
string nativeTarget = proj.TargetGuidByName(PBXProject.GetUnityTargetName());
string testTarget = proj.TargetGuidByName(PBXProject.GetUnityTestTargetName());
string[] buildTargets = new string[]{nativeTarget, testTarget};
proj.SetBuildProperty(buildTargets, "ENABLE_BITCODE", "NO");
File.WriteAllText(projPath, proj.WriteToString());
}
}
I too had the same issue "splash screen stuck" right after launch....
I solved this issue. Please use the below code.
Tested in Unity 5.4.1p2 and Xcode 7.3.
using UnityEngine;
using System.Collections;
using UnityEditor;
using System.IO;
using UnityEditor.Callbacks;
#if UNITY_IOS
using UnityEditor.iOS.Xcode;
#endif
public class Postprocessor : AssetPostprocessor
{
#if UNITY_IOS
[PostProcessBuild]
public static void OnPostprocessBuild(BuildTarget buildTarget, string path)
{
if (buildTarget == BuildTarget.iOS)
{
string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
PBXProject proj = new PBXProject();
proj.ReadFromString(File.ReadAllText(projPath));
string target = proj.TargetGuidByName("Unity-iPhone");
proj.SetBuildProperty(target, "ENABLE_BITCODE", "false");
File.WriteAllText(projPath, proj.WriteToString());
// Add url schema to plist file
string plistPath = path + "/Info.plist";
PlistDocument plist = new PlistDocument();
plist.ReadFromString(File.ReadAllText(plistPath));
// Get root
PlistElementDict rootDict = plist.root;
rootDict.SetBoolean("UIRequiresFullScreen",true);
plist.WriteToFile(plistPath);
}
}
#endif
}
In fact OnPostprocessBuild is always called, so you don't have to put anything in post export method field, which is designed for more specific methods.

C# Generating dynamic executable from project

I wanna generate an exe file with some changes in code from another C# exe.
I know that can easy compile .cs single class using CodeDom.Compiler
The thing I want to know is how to compile a project with 'Resources', 'Settings', 'Forms' and other elements.
CSharpCodeProvider.CompileAssemblyFromSource(CompilerParameters, sources[]);
So, the question is where can I add all resources, settings and form (.resx)?
And can I do it with byte[] streams. Without unpacking project's zip.
Sorry for bad English and mby stupid questions. I wish somebody will help me...
For Example: I have byte[] array of resource file 'pic.png' and I wanna attach it to compiled exe as embedded resource.
You should learn about the new compiler service provided by Microsoft in Microsoft.CodeAnalysis code name "Roslyn".
Roslyn provides you the way to compile the code and everything on the fly including creating and compiling complete solution and projects in-memory.
I think what you're looking for can be achieved via Roslyn. See below sample:
class Program
{
static void Main()
{
var syntaxTree = SyntaxTree.ParseCompilationUnit(
#"using System;
using System.Resources;
namespace ResSample
{
class Program
{
static void Main()
{
ResourceManager resMan = new ResourceManager(""ResSample.Res1"", typeof(Program).Assembly);
Console.WriteLine(resMan.GetString(""String1""));
}
}
}");
var comp = Compilation.Create("ResTest.exe")
.AddReferences(new AssemblyNameReference("mscorlib"))
.AddSyntaxTrees(syntaxTree);
var resourcePath = "ResSample.Res1.resources"; //Provide full path to resource file here
var resourceDescription = new ResourceDescription(
resourceName: "ResSample.Res1.resources",
dataProvider: () => File.OpenRead(resourcePath),
isPublic: false);
var emitResult = comp.Emit(
executableStream: File.Create("ResTest.exe"),
manifestResources: new[] { resourceDescription });
Debug.Assert(emitResult.Success);
}
Original Source here
At line dataProvider: () => File.OpenRead(resourcePath), you can provide your own 'FileStream' like () => return _myResourceStream) for your resource file.

BundleTransformer.Less inject variables depending on context/request

We would like the use the bundling mechanism of System.Web.Optimization in combination with the Less transformer.
The problem is that the same application/server serves pages for different branded websites. So depending on the 'SiteContext' the same .less files are used but different values should be used by the .less variables. So we want the (re)use the same less files but with different variables depending on the context of the request.
I tried a couple of different theories:
In all 3 cases I setup different bundles depending on the SiteContext.
1 inject an #import directive with the themed variables by using a custom VirtualPathProvider that intercepts the variables.less file.
So I have:
the styling file eg: header.less (imports the variables file)
the variables file: variables.less
a themed variables file: variables-theme.less (injected in variables.less via the VirtualPathProvider)
This is not working because the BundleTransformer cache sees this as the same file and doesn't know about the SiteContext. The cache key is based on the Url of the IAsset and we cannot influence this behavior.
2 Replace the variables.less import by variables-themed.less with an custom transformer that runs before the Less transformer.
Again no luck, same caching issues.
And as a side effect, the extra transformer was not called in debug because the assets are not bundled but called individually by the LessAssetHandler. This could be solved by writing your own AssetHandler that calls all required transformers.
3 create themed Asset names that are resolved by a custom VirtualPathProvider
Eg. Add header-themeX.less to the bundle, this file doesn't exist but you resolve this file to header.less and use method 2 to set the correct variables file import. (replace the import of the variables.less to the themed version).
Once again no luck. I think this could solve the caching issue if it wasn't for the Bundle.Include(string virtualPath) that does a File.Exists(path) internally. It doesn't pass via the CustomVirtualPathProvider.
Am I looking to deep to solve this?
All ideas are welcome, I can imagine that this will become a problem to more and more people as the System.Web.Optimization library gets more popular...
Keep in mind that:
we have a lot of .less/css files
we will have 5 or so themes
we like to keep things working in visual studio (that is why header.less has a ref. to variables.less)
Thanks for any feedback.
Michael!
You use the Microsoft ASP.NET Web Optimization Framework and the Bundle Transformer in multi-tenant environment, so you need to replace some components of the System.Web.Optimization and create own versions of the debugging HTTP-handlers (see «Problem: LESS file imports are added to BundleResponse.Files collection» discussion). As far as I know, Murat Cakir solve all these problems in the SmartStore.NET project.
In the Bundle Transformer there are 2 ways to inject of LESS-variables:
Look a properties GlobalVariables and ModifyVariables of LESS-translator:
using System.Collections.Generic;
using System.Web.Optimization;
using BundleTransformer.Core.Builders;
using BundleTransformer.Core.Orderers;
using BundleTransformer.Core.Transformers;
using BundleTransformer.Core.Translators;
using BundleTransformer.Less.Translators;
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
var nullBuilder = new NullBuilder();
var nullOrderer = new NullOrderer();
var lessTranslator = new LessTranslator
{
GlobalVariables = "my-variable='Hurrah!'",
ModifyVariables = "font-family-base='Comic Sans MS';body-bg=lime;font-size-h1=50px"
};
var cssTransformer = new CssTransformer(new List<ITranslator>{ lessTranslator });
var commonStylesBundle = new Bundle("~/Bundles/BootstrapStyles");
commonStylesBundle.Include(
"~/Content/less/bootstrap-3.1.1/bootstrap.less");
commonStylesBundle.Builder = nullBuilder;
commonStylesBundle.Transforms.Add(cssTransformer);
commonStylesBundle.Orderer = nullOrderer;
bundles.Add(commonStylesBundle);
}
}
Create a custom item transformation:
using System.Text;
using System.Web.Optimization;
public sealed class InjectContentItemTransform : IItemTransform
{
private readonly string _beforeContent;
private readonly string _afterContent;
public InjectContentItemTransform(string beforeContent, string afterContent)
{
_beforeContent = beforeContent ?? string.Empty;
_afterContent = afterContent ?? string.Empty;
}
public string Process(string includedVirtualPath, string input)
{
if (_beforeContent.Length == 0 && _afterContent.Length == 0)
{
return input;
}
var contentBuilder = new StringBuilder();
if (_beforeContent.Length > 0)
{
contentBuilder.AppendLine(_beforeContent);
}
contentBuilder.AppendLine(input);
if (_afterContent.Length > 0)
{
contentBuilder.AppendLine(_afterContent);
}
return contentBuilder.ToString();
}
}
And register this transformation as follows:
using System.Web.Optimization;
using BundleTransformer.Core.Orderers;
using BundleTransformer.Core.Bundles;
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
var nullOrderer = new NullOrderer();
const string beforeLessCodeToInject = #"#my-variable: 'Hurrah!';";
const string afterLessCodeToInject = #"#font-family-base: 'Comic Sans MS';
#body-bg: lime;
#font-size-h1: 50px;";
var commonStylesBundle = new CustomStyleBundle("~/Bundles/BootstrapStyles");
commonStylesBundle.Include(
"~/Content/less/bootstrap-3.1.1/bootstrap.less",
new InjectContentItemTransform(beforeLessCodeToInject, afterLessCodeToInject));
commonStylesBundle.Orderer = nullOrderer;
bundles.Add(commonStylesBundle);
}
}
Both ways have disadvantage: the injection of LESS-variables does not work in debug mode.

Categories