2
\$\begingroup\$

Recently, I've explored using rust as a scripting language for unity. My current method involves creating a DLL from rust, and loading it into my game at runtime. It requires that the user use a library I've made which has specific datatypes that I use to link unity and rust together, and must also be compiled as a library cdylib. I've thought about ways I can try and improve this since I'm not entirely sure if the method I've decided to use is the cleanest or most efficient.

example of usage:

use runity::{DataStruct, Vector3, String};

/* We now define some functions
   These are core functions that if not included,
   will not be run.
*/

// This function is called when the script is first run
#[no_mangle]
pub extern "C" fn awake(mut _data: DataStruct) -> DataStruct{
    _data
}

// This function is called after awake, use it to initialize values and setup the rest of the script
#[no_mangle]
pub extern "C" fn start(mut data: DataStruct) -> DataStruct{
    data.transform.position = Vector3::translate(data.transform.position, Vector3::new(0.0, 5.0, 0.0));
    data
}

// This function is run every frame
#[no_mangle]
pub extern "C" fn update(mut data: DataStruct) -> DataStruct{
    let tag = String::from_str("Player").unwrap(); // Get the tag for "Player"

    let player_obj = data.game_object.get_gameobject_from_tag(tag); // Get the game object associated with the tag

    let pos_to_go_towards = player_obj.transform.position; // get the position of the player

    data.transform.position = Vector3::lerp(data.transform.position, pos_to_go_towards, 0.00015); // move towards the player using lerp

    data
}

Another way I'd like to improve is the way I manage the loaded DLLs from within unity. These scripts highlight my current method I use:

using System.Runtime.InteropServices;
using System;
using UnityEngine;
using TMPro;
using System.Collections.Generic;
using UnityEngine.Scripting;

[assembly: Preserve]

/*   This script acts as a sort of buffer
    between the DLL (used for scripting)
    and the unity engine. 
    
    This script should be attached to the game object you want the script to affect.
*/

namespace runity_test
{
    public class runity: MonoBehaviour
    {
        /* Variables */

        // Our DLL name
        public string DLLName = "runity.dll";

        // Error text
        public TMP_Text errorText;

        // A copy of our position
        public Vector3 position;

        // Some variables to store per update info
        public GameObject m_gameObject;
        public Transform m_transform;
        public DataStruct dataStruct;

        // Our function pointers, so we can recycle them rather than waste
        // processing time reloading the DLL
        StartDelegate start;
        UpdateDelegate update;

        // A pointer to our loaded DLL so we can free it on exit
        IntPtr loadedDLLPtr;

        // An object pool to avoid calling Find on gameobjects every frame
        Dictionary<string, UnityEngine.GameObject> objectPool = new Dictionary<string, UnityEngine.GameObject>();

        // We use these booleans to check if we should run the respective unity functions.
        // This is so we can check collisions conditionally.
        bool runStart;
        bool runUpdate;

        /* Import our functions (start, update and awake) as delegates, as they will be pointers to the functions
         * since we want to load them dynamically at runtime 
         */

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate DataStruct StartDelegate(DataStruct data);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate DataStruct UpdateDelegate(DataStruct data);


        /* Define the structs to use to interface with rust with. This gives
         * the option to implement safe, rust compatiable datatypes which
         * we can't guarantee with unity */

        [StructLayout(LayoutKind.Sequential)]
        public struct Vector3
        {
            public float x;
            public float y;
            public float z;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct Transform
        {
            public Vector3 position;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct GameObject
        {
            public IntPtr tag;
            public Transform transform;
            public FindGameObjectWithTagDelegate GetGameObjectByTag;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct DataStruct
        {
            public Transform transform;
            public GameObject gameObject;
        }


        /* Define our delegates, which are callbacks to functions we want to use 
         * in rust */
        public delegate GameObject FindGameObjectWithTagDelegate(IntPtr name); // This delegate acts as FindGameObjectWithTag


        /* Run built-in unity functions */

        void Awake()
        {
            (Delegate startFunction, IntPtr startPtr) = DLLPool.LoadFunctionFromDLL(DLLName, "start", typeof(StartDelegate));

            if (startPtr != IntPtr.Zero)
            {
                Debug.Log("Running function!");
                runStart = true;
                start = (StartDelegate)startFunction;
            }


            (Delegate updateFunction, IntPtr updatePtr) = DLLPool.LoadFunctionFromDLL(DLLName, "update", typeof(UpdateDelegate));

            if (updatePtr != IntPtr.Zero)
            {
                Debug.Log("Running function!");
                runUpdate = true;
                update = (UpdateDelegate)updateFunction;
            }



            // We now assign our delegates to point to our functions.

            // Initialize values with no value (should be set in Rust's start function)
            m_transform = new Transform { };
            m_gameObject = new GameObject { };
            dataStruct = new DataStruct { };
        }


        // Start is called before the first frame update
        void Start()
        {
            if (runStart)
            {
                m_transform.position = new Vector3 { x = 0, y = 0, z = 0 };

                m_gameObject.transform = m_transform;
                m_gameObject.GetGameObjectByTag = new FindGameObjectWithTagDelegate(GetGameObjectFromTag);

                dataStruct.transform = m_transform;
                dataStruct.gameObject = m_gameObject;


                dataStruct = start(dataStruct);

                m_gameObject = dataStruct.gameObject;
                m_transform = dataStruct.transform;
                position = m_transform.position;

                NativeMethods.Free(dataStruct.gameObject.tag);
            }
        }

        // Update is called once per frame
        void Update()
        {
            if (runUpdate)
            {
                m_gameObject.transform = m_transform;

                dataStruct.transform = m_transform;
                dataStruct.gameObject = m_gameObject;


                dataStruct = update(dataStruct);


                m_gameObject = dataStruct.gameObject;
                m_transform = dataStruct.transform;
                position = m_transform.position;

                transform.position = new UnityEngine.Vector3(position.x, position.y, position.z);

                NativeMethods.Free(dataStruct.gameObject.tag);
            }
        }

        // This function releases all our pointers to remain safe
        private void OnDestroy()
        {
            // This is VERY important, we must free and release the link before we exit!
            NativeMethods.Free(m_gameObject.tag);
            Debug.Log("Released pointers properly");

            DLLPool.UnloadAll();
        }


        /* We can now define the functions we want to expose to rust */

        // This method converts `FindGameObjectWithTag` into our custom defined structs
        public GameObject GetGameObjectFromTag(IntPtr tag)
        {
            GameObject gameObject = new GameObject { transform = new Transform { position = new Vector3 { x = 0, y = 0, z = 0, } } };

            // We check if the object with its tag is not already pooled. If it is, we make sure it hasn't been destroyed and then take it from the pool.
            // otherwise, we load it and add it to the pool
            if (objectPool.ContainsKey(Marshal.PtrToStringAnsi(tag)))
            {
                UnityEngine.GameObject foundObj = objectPool[Marshal.PtrToStringAnsi(tag)];
                if (foundObj == null)
                {
                    foundObj = UnityEngine.GameObject.FindGameObjectWithTag(Marshal.PtrToStringAnsi(tag));
                    Transform transform = new Transform { position = new Vector3 { x = foundObj.transform.position.x, y = foundObj.transform.position.y, z = foundObj.transform.position.z } };
                    gameObject.transform = transform;
                    gameObject.tag = tag;
                    objectPool.Add(Marshal.PtrToStringAnsi(tag), foundObj);
                }
                else
                {
                    Transform transform = new Transform { position = new Vector3 { x = foundObj.transform.position.x, y = foundObj.transform.position.y, z = foundObj.transform.position.z } };
                    gameObject.transform = transform;
                    gameObject.tag = tag;
                }
            }
            else
            {
                UnityEngine.GameObject foundObj = UnityEngine.GameObject.FindGameObjectWithTag(Marshal.PtrToStringAnsi(tag));
                if (foundObj == null)
                {
                    Debug.LogWarning("Warning: Tag -> " + Marshal.PtrToStringAnsi(tag) + " was not found. Falling back to default transform. ");
                    Transform transform = new Transform { position = new Vector3 { x = 0, y = 0, z = 0 } };
                    gameObject.transform = transform;
                    gameObject.tag = tag;
                }
                else
                {
                    Transform transform = new Transform { position = new Vector3 { x = foundObj.transform.position.x, y = foundObj.transform.position.y, z = foundObj.transform.position.z } };
                    gameObject.transform = transform;
                    gameObject.tag = tag;
                    objectPool.Add(Marshal.PtrToStringAnsi(tag), foundObj);
                }
            }

            return gameObject;
        }
    }

}
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System;
using UnityEngine;

/*
    This class will pool loaded DLL's to save from loading the same DLL multiple times.
    It will also manage the unloading of said DLL's (This must be called however)
    Attach this script to a gameobject.
*/

namespace runity_test
{
    /// <summary>
    /// This class contains some functions from the native windows library
    /// to help us load and release libraries at runtime.
    /// </summary>
    static class NativeMethods
    {
        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string dllToLoad);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);

        [DllImport("kernel32.dll")]
        public static extern bool FreeLibrary(IntPtr hModule);

        public static void Free(IntPtr ptr)
        {
            Marshal.FreeCoTaskMem(ptr);
        }
    }

    public class DLLPool: MonoBehaviour
    {
        public static Dictionary<string, IntPtr> dllPool = new Dictionary<string, IntPtr>();

        private static DLLPool instance;
        public static DLLPool Instance { get { return instance; } }

        private void Awake()
        {
            if (instance != null && instance != this)
            {
                Destroy(this.gameObject);
            }
            else
            {
                instance = this;
            }
        }
        
        /// <summary>
        /// This function loads a DLL from the stored dictionary.
        /// 
        /// This helps you save time by loading DLL's loaded already, rather than load them again
        /// </summary>
        /// <param name="dllName"></param>
        /// <returns></returns>
        public static IntPtr GetDLL(string dllName)
        {
            if (dllPool.ContainsKey(dllName))
            {
                return dllPool[dllName];
            }

            return IntPtr.Zero;
        }

        /// <summary>
        /// This function takes the name of the DLL to load
        /// 
        /// These DLL's are expected to be stored in the `Assets/Plugins/` folder
        /// </summary>
        /// <param name="dllName"></param>
        /// <returns></returns>
        public static IntPtr LoadDLL(string dllName)
        {
            if (GetDLL(dllName) != IntPtr.Zero)
            {
                return GetDLL(dllName);
            }

            //Get the path of the Game data folder
            string m_Path = Application.dataPath;

#if UNITY_EDITOR
            string path = m_Path + "/Plugins/" + dllName;
#else
            string path = m_Path + "/Plugins/x86_64/" + dllName;
#endif
            Debug.Log("Loading DLL: " + path);
            // Load the DLL library
            IntPtr loadedDLLPtr = NativeMethods.LoadLibrary(path);
            if (loadedDLLPtr == IntPtr.Zero)
            {
                Debug.LogError("Error! The library " + dllName + " couldn't be found!");
                return IntPtr.Zero;
            }
            else
            {
                Debug.Log("Successfully loaded DLL: " + path);
            }

            dllPool.Add(dllName, loadedDLLPtr);

            return loadedDLLPtr;
        }

        public static (Delegate, IntPtr) LoadFunctionFromDLL(string dllName, string functionName, Type delegateType)
        {
            try
            {
                if (GetDLL(dllName) != IntPtr.Zero)
                {
                    Debug.Log("DLL exists in pool, loading from pool...");
                    IntPtr dllPtr = GetDLL(dllName);

                    IntPtr functionPtr = NativeMethods.GetProcAddress(dllPtr, "update");
                    if (functionPtr == IntPtr.Zero)
                    {
                        Debug.LogWarning("Couldn't find function '" + functionName + "' in " + dllName + ", '" + functionName + "' in unity won't be run!");
                    }
                    Delegate function = Marshal.GetDelegateForFunctionPointer(functionPtr, delegateType);
                    Debug.Log("Loaded function '" + functionName + "' from " + dllName + " successfully!");
                    return (function, functionPtr);
                }
                else
                {
                    Debug.Log("DLL not loaded, loading DLL...");
                    IntPtr dllPtr = LoadDLL(dllName);

                    IntPtr functionPtr = NativeMethods.GetProcAddress(dllPtr, "update");
                    if (functionPtr == IntPtr.Zero)
                    {
                        Debug.LogWarning("Couldn't find function '" + functionName + "' in " + dllName + ", '" + functionName + "' in unity won't be run!");
                    }
                    Delegate function = Marshal.GetDelegateForFunctionPointer(functionPtr, delegateType);
                    Debug.Log("Loaded function '" + functionName + "' from " + dllName + " successfully!");
                    return (function, functionPtr);
                }
            }
            catch (Exception e)
            {
                Debug.Log("Error when loading function from DLL - " + e);
                return (null, IntPtr.Zero);
            }
        }

        public static void UnloadAll()
        {
            Debug.Log("Total DLLs currently loaded: " + dllPool.Count);
            // This is important - free all our loaded libraries
            foreach (string name in dllPool.Keys)
            {
                Debug.Log("Releasing DLL: " + name);
                NativeMethods.FreeLibrary(dllPool[name]);

            }
        }
    }
}

The link to the repo (if you'd like to take a look at the way I've implemented the datatypes, too many files to copy here): https://github.com/dimitribobkov/runity

Thanks for the help in advance :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.