Search Unity

[SOLVED]Inserting DLL into built game

Discussion in 'Scripting' started by Laperen, Mar 4, 2019.

  1. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    1,065
    My goal is to allow user created content to be included as a mod, after the game has been built. My understanding is that AssetBundles can contain pretty much anything but code, and is not a problem for patching as long as the code already exists in the base game.

    But if say you need completely new behaviour for a mod, writing new scripts is inevitable. I reasoned that if I included the assets in an AssetBundle and scripts in a C# DLL, I could insert the AssetBundle and DLL into a built game and have them work normally

    In practice this unfortunately wasn't the case. The AssetBundle worked fine as it was instructed too, but was not at able to use code in the newly inserted DLL. The DLL was placed in the Managed folder, while the AssetBundle was placed in the StreamingAssets folder.

    My question is, is it possible to include a DLL in a built game and have it used by new assets introduced via AssetBundles?

    I've already searched out most of the low hanging fruit I believe, having content creators write Lua and/or Python scripts instead, and this link that I've seen pop up far too many times:
    https://www.angryant.com/2010/01/05/downloading-the-hydra/
    Problem with the link's solution is that the DLL needs to be loaded in, which is fine for everything but MonoBehaviours since they cannot be used conventionally the way Unity does with attaching it on a GameObject in-editor.

    Also, when I build a game with the DLL already included, the asset bundle works fine being able to access code in the DLL. If what I want can't be accomplished, I'd at least want to know why including a DLL in a built game can't be done.
     
  2. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    Have you tried loading a DLL with Assembly.LoadAssembly and calling GameObject.AddComponent with MethodInfo.Ivoke using a type from that loaded DLL as a generic argument?
     
  3. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    1,065
    Yes I have tried that before, but that will not allow the monobehaviour scripts of the DLL to be placed in a scene or prefab of an assetbundle, at least not without existing infrastructure from the base game.

    My challenge is to do the above, without existing infrastructure from the base game, which is why loaded assembly isnt an option.
     
  4. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    542
    You want to make the game modable by the user but at the same time you want to include the scripts in your scene/prefabs? How would you add a script in your project scene, when the script is made by the user?
    You have to give a more specific example what you actually want.

    I have used asset bundles (for 3d assets) and mod dlls (with load assembly) and everything worked fine.

    Let's say the user/player wants to make their own mod/dll:
    - First they have to make a C# mod.dll
    - They make a Unity project and add the mod.dll now they can use the scripts in their scene and include them in their asset bundle
    - When they are done, they have a mod asset bundle and mod dll

    Now in the base game you have to add logic to:
    - FIRST load the mod.dll (for example with the assembly load methods)
    - and AFTER that you load the asset bundle (otherwise it doesn't find the required classes)

    (Obviously all versions like Unity and .NET etc have to match, otherwise it doesn't work)
     
    Last edited: Mar 4, 2019
  5. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    1,065
    Maybe say, the game is a basic FPS, but now a content creator wants to add vehicles in. I would expect the vehicles to need new scripts to operate. The vehicles would be prefabs and new scenes would be levels to play with the vehicles in.

    Do you have an example code snipplet of how you would make use of a DLL loaded via assembly load methods? Although if its the same as this link's method, how would a new scene and/or prefab make use of it? it feels like adding component during runtime would make it impossible to allow values to be put in inspector while in-editor.
     
  6. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    I'd create rather generic prefabs all controlled via run time scripting, with models and textures loaded at runtime. The modders don't actually create any MonoBehaviour scripts, any GameObjects, or prefabs. Just have your generic prefab MonoBehaviours call the run time scripted methods to control them. No DLL's, no AssetBundles, etc.

    So in the case of adding vehicles, create a generic vehicle prefab. You don't even need to include actual vehicles in the base game, just make it available for the modders.
     
  7. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    1,065
    I'm not actually requiring a vehicle in my game, it was just an example. Even then, using that example, it goes beyond simply including a vehicle. Maybe the content creator wants to overlay their own UI on it, include custom obstacles for their own challenges. All these things will probably require their own unique behaviours which I cannot possibly account for to build generic prefabs as tools for all of them.
     
  8. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    542
    I can try to make a small example setup at the weekend, but until then I don't have enough time.

    Usually you load a dll and then you get some specific class or interface and call it's methods, but this is not needed in this case.

    As I posted above, the user would create their own c# dll with their own component, then they would drag the dll in their local Unity project (I don't remember, they might have to drag them in a "Plugin" folder, but I think for C# dlls it doesn't matter in which folder they are?), now they have access to all components in the dll and they can just add them to their scene/prefabs. Now they can export their asset bundle. They have to use the asset bundle + the dll for your mod.

    In your game you can load the asset bundle, but it won't work, because it doesn't know the types/component they used. So instead you first have to load the mod dll (you don't have to do anything else, just load the assembly so that the types are loaded) and after that you can load the asset bundle, now it will find/know the types, because the dll was already loaded.

    This way the user can create their own dlls, but if they need access to your scripts, for example let's say you have a "Weapon" class and the mod should have access to this class, then you have to make a "common" dll with classes/interfaces that you want to expose to the modder.

    Btw I assume that you work on a windows game, otherwise it's more complicated.
     
  9. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    1,065
    That would be very kind, thank you for the offer at all.
    As far as using a DLL while you are still working on the project in unity is concerned, the DLL just needs to be somewhere within the Assets folder or Sub-Folders within the Assets Folder, as long as it isn't in a specially marked folders like AssetBundles, StreamingAssets, or even Resources IIRC. To add to this, when the game is built, the DLL is thrown wholesale into the Managed folder for a built game.
    This is the best news I've read within the last 2 weeks. I think I'm only having trouble with this part, loading the DLL and having the accompanying AssetBundle recognise it.

    I'm also planning for the AssetBundle to contain the DLL as a TextAsset and have it created via System.IO methods, or in the case of loading, the TextAsset can be read directly as a byte array. is this a bad idea?
    Yes the game is for windows
     
  10. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    1,065
    Update, I've managed to load a DLL stored as a text asset, and have it recognised by its accompanying AssetBundle.

    Thank you R1PFake for the clues.

    It was far simpler than I expected, only needing to use byte data from the DLL as a TextAsset in System.Reflection.Assembly.Load to get Unity and the AssetBundle to recognise the DLL.

    As of yet I still do not know if storing a DLL as a TextAsset in an AssetBundle is a bad idea. Only when a DLL gets big enough will i know if this is a good or bad idea. With that said, if the DLL needs to be loaded in for use anyway, the format it is stored in should not matter IMO.
     
  11. drorriov

    drorriov

    Joined:
    Jun 7, 2014
    Posts:
    43
    Note that approach worked for me also, however, when porting to android and using IL2CPP you will get an error:
    "This icall is not supported by il2cpp." in Assembly.Load(txt.bytes);

    It seems reflecting a dll is unusable much on mobile as also IOS also use AOT, unless something changed that I am not aware of.
     
  12. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    542
    Im not sure about android, but I think the only way to "add" code at runtime on IOS is to use a scripting language which will be interpreted at runtime (maybe a lua plugin, or your own language if you only need a few features)
     
  13. alexniver

    alexniver

    Joined:
    Mar 8, 2014
    Posts:
    6
    hi, can you help me? i'm stuck with 'have text asset dll recognised by its accompanying AssetBundle', how to do this with code? I have done 'store dll as TextAsset in AssetBundle, use Assembly.Load load the dll', but the Prefab in AssetBundle use this dll can't find the class I have load. error info 'The referenced script (PlayerController) on this Behaviour is missing!'
     
  14. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    1,065
    It has been awhile since I implemented this, I will have to refresh my memory on it. From what I recall, assuming the dll has been made properly; as long as you used Assembly.load once on the dll, the dll will be usable throughout that run of the application's session. I will need to see your script to see if there are any other problems.
     
  15. alexniver

    alexniver

    Joined:
    Mar 8, 2014
    Posts:
    6
    Thank you for help!

    here is my load script code.

    I create two assetBundle, one for script, one for other. and I load script first, the load other.

    I have test , script can load

    when it runs to 'var prefab = assetBundle.LoadAsset<GameObject>(name);' it error 'The referenced script (PlayerController) on this Behaviour is missing!'

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using System.Reflection;
    5. using System;
    6.  
    7. public class LoadAssetBundleScript : MonoBehaviour {
    8.     AssetBundle assetBundleScript;
    9.     AssetBundle assetBundle;
    10.  
    11.     // Start is called before the first frame update
    12.     void Start() {
    13.  
    14.         assetBundleScript = LoadAssetBundle(@"D:\Deployment\unity\AssetBundleTest\AssetBundles\testmodscript");
    15.  
    16.         LoadDll();
    17.  
    18.         assetBundle = LoadAssetBundle(@"D:\Deployment\unity\AssetBundleTest\AssetBundles\testmod");
    19.  
    20.         InstaniateGameObject("CharacterPrefab");
    21.     }
    22.  
    23.     AssetBundle LoadAssetBundle(string filePath) {
    24.         return AssetBundle.LoadFromFile(filePath);
    25.     }
    26.  
    27.     void LoadDll() {
    28.         foreach(string assetName in assetBundleScript.GetAllAssetNames()) {
    29.             if(assetName.EndsWith("bytes")) {
    30.                 Debug.Log(assetName);
    31.  
    32.                 TextAsset txt = assetBundleScript.LoadAsset<TextAsset>(assetName);
    33.  
    34.                 Assembly assembly = Assembly.Load(txt.bytes);
    35.  
    36.             }
    37.         }
    38.     }
    39.  
    40.     void InstaniateGameObject(string name) {
    41.         var prefab = assetBundle.LoadAsset<GameObject>(name);
    42.         Instantiate(prefab);
    43.     }
    44. }
    45.  

    here is my code for create script dll asset bundle TextAsset


    Code (CSharp):
    1. using System.CodeDom.Compiler;
    2. using System.IO;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using UnityEngine;
    6. using UnityEditor;
    7. using Microsoft.CSharp;
    8. using System;
    9.  
    10. public class AssetBundleEditor : Editor {
    11.    
    12.     [MenuItem("Assets/ Build AssetsBundles")]
    13.     static void BuildAllAssetBundle() {
    14.         CompileDll();
    15.         BuildPipeline.BuildAssetBundles(@"D:\Deployment\unity\AssetBundleTest\AssetBundles\", BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows64);
    16.     }
    17.  
    18.     static void CompileDll() {
    19.         string path = Path.Combine(Application.dataPath, "Mod");
    20.         if(!Directory.Exists(path)) {
    21.             Debug.LogError("can not compile dll, no 'Mod' floder");
    22.             return;
    23.         }
    24.         DirectoryInfo dir = new DirectoryInfo(path);
    25.  
    26.         List<FileInfo> allSourceFile = GetAllSourceFile(dir);
    27.  
    28.         CSharpCodeProvider codeProvider = new CSharpCodeProvider();
    29.  
    30.         string assemblyPath = Path.Combine(path, "assembly");
    31.         if(!Directory.Exists(assemblyPath)) {
    32.             Directory.CreateDirectory(assemblyPath);
    33.         }
    34.  
    35.         foreach(FileInfo fileInfo in allSourceFile) {
    36.             Debug.Log(fileInfo.FullName + " start compile");
    37.  
    38.             CompilerParameters parameters = new CompilerParameters();
    39.             parameters.GenerateExecutable = false;
    40.             parameters.OutputAssembly = Path.Combine(assemblyPath, fileInfo.Name.Substring(0, fileInfo.Name.LastIndexOf(".")) + ".bytes");
    41.             foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
    42.                 parameters.ReferencedAssemblies.Add(assembly.Location);
    43.             }
    44.            
    45.             CompilerResults results = codeProvider.CompileAssemblyFromFile(parameters, fileInfo.FullName);
    46.  
    47.             if (results.Errors.Count > 0) {
    48.                 foreach(CompilerError compilerError in results.Errors) {
    49.                     if(!compilerError.IsWarning) {
    50.                         Debug.LogError(compilerError);
    51.                     } else {
    52.                         Debug.LogWarning(compilerError);
    53.                     }
    54.                 }
    55.             } else {
    56.                 Debug.Log(fileInfo.FullName + " compile dll success");
    57.             }
    58.         }
    59.     }
    60.  
    61.     private static List<FileInfo> GetAllSourceFile(DirectoryInfo rootDir) {
    62.         List<FileInfo> result = new List<FileInfo>();
    63.         foreach(DirectoryInfo dirInfo in rootDir.GetDirectories()) {
    64.             result.AddRange(GetAllSourceFile(dirInfo));
    65.         }
    66.         foreach(FileInfo fileInfo in rootDir.GetFiles()) {
    67.             if(fileInfo.Name.EndsWith(".cs")) {
    68.                 result.Add(fileInfo);
    69.             }
    70.         }
    71.         return result;
    72.     }
    73.    
    74. }
    75.  
     
  16. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    1,065
    Oh don't thank me yet. I'm not in a situation where I can investigate this in detail Immediately I'm afraid. I'll make an effort this weekend, hopefully that wont be too late. At least with your scripts in public view, others can chime in.

    For now, help me test, this part of your script
    Code (CSharp):
    1. void Start() {
    2.  
    3.     assetBundleScript = LoadAssetBundle(@"D:\Deployment\unity\AssetBundleTest\AssetBundles\testmodscript");
    4.  
    5.     LoadDll();
    6.  
    7.     assetBundle = LoadAssetBundle(@"D:\Deployment\unity\AssetBundleTest\AssetBundles\testmod");
    8.  
    9.     InstaniateGameObject("CharacterPrefab");
    10. }
    11.  
    try calling the loadDLL in Awake and the rest in start
    Code (CSharp):
    1. void Awake() {
    2.     assetBundleScript = LoadAssetBundle(@"D:\Deployment\unity\AssetBundleTest\AssetBundles\testmodscript");
    3.  
    4.     LoadDll();
    5. }
    6. void Start() {
    7.     assetBundle = LoadAssetBundle(@"D:\Deployment\unity\AssetBundleTest\AssetBundles\testmod");
    8.  
    9.     InstaniateGameObject("CharacterPrefab");
    10. }
    11.  
     
  17. alexniver

    alexniver

    Joined:
    Mar 8, 2014
    Posts:
    6
    hi, test finished. Change to Awake not work.
     
  18. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    542
    I don't remember all the details, but I think there is a difference in your workflow.
    I looks like you include the .cs files in your Unity mod project and compile it from there.
    If I remember correctly @Laperen built the DLL outside of Unity and included the built DLL to the Unity mod project.

    I checked your BuildAllAssetBundle method and I see that you compile the DLL there, but where do you add the built DLL to the asset bundle? Are you sure that your AssetBundle contains the DLL?

    Currently you compile every file to it's own DLL, this will only work if the scripts don't reference each other, you can use the other method of the code provider to build multiple files to a single dll. I don't remember the exact method name, but it should have a similar name and it takes a array of files instead of just one.


    Just to make sure you can try these steps:
    - Make a backup copy of your project(s) just to be sure

    - Create a external C# project (depending on your IDE; for example in Visual Studio select the library project type) -> Important make sure that the .NET framework version matches with your Unity .NET version (for example .NET 4.5+ or .NET Standard 2)

    - Your project will most likely need a reference to the UnityEngine.dll (and optionally others), the path of the UnityEngine.dll is different depending on your Unity version, google search should provide the correct path for your specific version (it's usually somewhere in the Unity install folder)

    - Move all your mod script to the external project and delete them from your Unity project copy

    - Compile the external C# dll with your IDE (I use Visual Studio)

    - Now import (copy) the compiled DLL to your Unity mod project (you may have to re assign your scripts to the prefabs after this step)

    - Adjust your asset bundle code (don't build a new DLL during the AssetBundleBuild)


    - After all these steps it should work with the (external) mod DLL and we can be sure that the compiling / loading of the DLL work, then we can try to figure out why it doesn't work if you build it inside Unity (if you still need this step)
     
    Last edited: Nov 26, 2020
    Laperen likes this.
  19. alexniver

    alexniver

    Joined:
    Mar 8, 2014
    Posts:
    6
    Really thanks a lot! I'll try! Love U!
     
  20. xlenz

    xlenz

    Joined:
    Oct 13, 2019
    Posts:
    1
    @R1PFake @Laperen @alexniver

    I have a built app and time to time I would like to add Asset Bundles and the Scripts (instead of releasing a new version of the app). I use your above solution of late binding new dll code at runtime..

    1. Will it work on Android and iOS??
    2. Does appstore and playstore allow such apps to be published?
     
  21. bobisgod234

    bobisgod234

    Joined:
    Nov 15, 2016
    Posts:
    1,042
    It might be possible to get it running on Android if you select the Mono build option and ARM32 processor type. I don't know if modern Unity will allow you to make ARM32 builds anymore, and I am pretty sure the Play Store no longer accepts ARM32 apps.

    IOS is exceptionally strict about dynamically executing code. You cannot use the Mono runtime, and as such cannot load .NET DLLs.

    Android ARM64 and IOS require you to use IL2CPP, which is not capable of loading C# DLL's. I am pretty sure those builds entirely lack the Mono runtime, and as such it would be impossible to run a .NET DLL.
     
  22. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    225
    Dont need to have such complex setup, you can use Assembly Definitions for each package that will contain custom code + new assets.
    1. Create new Assembly Definition. Name it, for example, `Content01`
    2. Add any scripts you want, whatever, build your scene using these scripts.
    3. Once done, go into Library/ScriptAssemblies, find your custom assembly, import as Text Asset, assign it to the same asset or another asset bundle in the package and build AB.
    4. Now, runtime, you load AB, load that text asset and then load Assembly from memory/bytes. In order to make it dynamic and avoid collisions or issues with existing Text Assets, I recommend your loading code to be made generic, based on (main) bundle name or like the guy above uses `.bytes`. Important note: Assembly.Load(byte[] rawbytes) WILL load Assembly multiple times over, so add some basic safeguarding because Assembly wont be unloaded from AppDomain after bundle is unloaded.
    5. Now, you can load your prefabs and stuff. BECAUSE scripts on these new assets were confined to Assembly Definition assembly, they will be looking for that specific assembly and the first thing that happens is resolution via AppDomain, which will find our assembly with matching metadata.
     
    r3dux and R1PFake like this.
  23. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    542
    I never used Assembly Definitions before, but that sounds a lot easier, I will use these in the future, thanks for the hint
     
  24. paulbettner

    paulbettner

    Joined:
    Jan 13, 2014
    Posts:
    14
    Thank you @Digika!

    I am trying to get your approach working, by calling Assembly.Load and then loading a scene with a game object that references a component in the loaded assembly, but I'm still getting "The referenced script (Unknown) on this Behaviour is missing!" warnings.

    Basically, I have project A with that uses an assembly definition to compile scripts into a separate assembly (per your advice.) It has a scene with a simple test object that references one of the scripts.

    Then I copy the scene and the DLL over to a separate clean project B. Then on startup, project B first loads the assembly, then it loads the scene. But I still get "The referenced script (Unknown) on this Behaviour is missing!"

    How can I tell Unity to find the type it is looking for in my dynamically loaded assembly?

    Best,
    Paul
     
  25. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    225
    @paulbettner
    The workflow I've described is not supposed to work for this case, it only should work when you have a built game from project A and you want to add extra content made in completely different Project B that is not main game project. This assumes that built&compiled Project A has some generic code that can load bundles and find embedded assemblies in it first, before loading anything else that has serialized mono-references. There is probably a way to make it work in the Editor Project as well, havent looked into it but I know there are some assembly environment differences in editor. You can try copying your assembly into "Library/ScriptAssemblies" in project B before starting the Play Mode and loading the asset bundle in it.

    Try this:
    Open your bundle with something like
    https://github.com/DerPopo/UABE
    or
    https://github.com/nesrak1/UABEA

    Find your Monoscript base for your script (follow the fileID and pathID references from gameobject it is on), open it in view mode (View Data) and check what assembly and namespace it saved as a lookup reference.

    upload_2021-3-31_11-37-39.png
     
    Last edited: Mar 31, 2021
  26. paulbettner

    paulbettner

    Joined:
    Jan 13, 2014
    Posts:
    14
    Thank you for the help @Digika!
     
  27. Digika

    Digika

    Joined:
    Jan 7, 2018
    Posts:
    225
    Syn0_ and Allen0012 like this.