C
C#5mo ago
Mazranel

Implementing scripting in a game engine

What would be the best way to implement scripting in a game engine? I've been looking at reflection and runtime code compilation but I wasn't sure if they would give the user access to the engine's libraries. I was also thinking of implementing my own custom language like what Godot does, but my engine is so simple that I think it would be overkill. I'd like to do something similar to how big engines like Unity and Godot do it (attaching scripts to objects), is there an effective way to do that?
21 Replies
Mazranel
MazranelOP5mo ago
Maybe I could achieve the desired outcome by implementing Lua or python support?
Jimmacle
Jimmacle5mo ago
lua is common for ingame scripting you can do C# but you have to be very careful and basically whitelist exactly what types/members they're allowed to use, and without sandboxing they could easily break things
Mazranel
MazranelOP5mo ago
I think I've come up with a potential idea. My engine can just export scenes that the (sort of separate) libraries can make sense of Do you know of any libraries that can link the two together?
Jimmacle
Jimmacle5mo ago
not personally, you could ask #game-dev
Rettoph
Rettoph5mo ago
ive not used it a ton, but Moonsharp might be a good option https://github.com/moonsharp-devs/moonsharp
GitHub
GitHub - moonsharp-devs/moonsharp: An interpreter for the Lua langu...
An interpreter for the Lua language, written entirely in C# for the .NET, Mono, Xamarin and Unity3D platforms, including handy remote debugger facilities. - moonsharp-devs/moonsharp
Mazranel
MazranelOP5mo ago
I'll research moonsharp, it seems promising! I'll see if they have any other libraries along with moonsharp
cap5lut
cap5lut5mo ago
i think NLua is also often used
Valdiralita
Valdiralita5mo ago
space engineers has ingame blocks where you can execute c# code
Jimmacle
Jimmacle5mo ago
yes and they do the thing i suggested for that and afaik they frequently cause performance issues and have unfixable exploits due to the nature of C#
Mazranel
MazranelOP5mo ago
I think I'm going to try this first, and if it doesn't work I'll implement either Moonsharp / NLua or some python equivalent
SleepWellPupper
SleepWellPupper5mo ago
What about using a WASM sandbox of assemblies? Is that already a thing or still in preview?
SleepWellPupper
SleepWellPupper5mo ago
See here: https://www.youtube.com/watch?v=5u1UaqkPZbg Maybe you'll find it useful.
stevensandersonuk
YouTube
DotNetIsolator: an experimental package for running .NET code in an...
A quick overview and demo of an experimental new .NET package that can create isolated .NET runtime instances for running code in a sandbox.
Mazranel
MazranelOP4mo ago
If it's a thing already I might give it a shot. Would it give the user access to built-in libraries in my engine?
SleepWellPupper
SleepWellPupper4mo ago
I believe you have fine control over the types available to consumers. I didn't look into it that much but I assume it is possible.
Mazranel
MazranelOP4mo ago
I'll do some more research, this seems promising Unfortunately I couldn't get anything that had to do with Roslyn to work (didn't even install properly), so I'll have to do more looking around Maybe NLua is my only option for now if absolutely nothing else is working at the moment
SleepWellPupper
SleepWellPupper4mo ago
I could help you set it up. From my experience it seems straightforward. But I understand if by this point it would seem like too much effort to you.
Mazranel
MazranelOP4mo ago
That'd be cool How much of a pain is it to set up normally?
SleepWellPupper
SleepWellPupper4mo ago
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp;

using Microsoft.CodeAnalysis;
using DotNetIsolator;
using ConsoleApp1;
using Wasmtime;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp;

using Microsoft.CodeAnalysis;
using DotNetIsolator;
using ConsoleApp1;
using Wasmtime;
//set up source
const String source =
"""
namespace MyPluginPackage;
using ConsoleApp1;
using System;
public class Plugin : IPlugin
{
public void Execute(string arg)
{
Console.WriteLine($"Hello from plugin! Arg: {arg}");
}
}
""";

//parse using roslyn
var tree = CSharpSyntaxTree.ParseText(
source,
new CSharpParseOptions(
LanguageVersion.Latest,
DocumentationMode.None,
SourceCodeKind.Regular),
cancellationToken: CancellationToken.None);

//compile using roslyn
using var peStream = new MemoryStream();
var pluginAssemblyName = "PluginAssembly";
var emitResult = CSharpCompilation.Create(
assemblyName: pluginAssemblyName,
new SyntaxTree[] { tree },
[
MetadataReference.CreateFromFile(typeof(IPlugin).Assembly.Location),
..Basic.Reference.Assemblies.NetStandard20.References.All
],
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithOverflowChecks(true)
.WithOptimizationLevel(Microsoft.CodeAnalysis.OptimizationLevel.Release))
.Emit(peStream);

if(!emitResult.Success)
{
foreach(var diagnostic in emitResult.Diagnostics)
{
Console.WriteLine(diagnostic);
}

return;
}
//set up source
const String source =
"""
namespace MyPluginPackage;
using ConsoleApp1;
using System;
public class Plugin : IPlugin
{
public void Execute(string arg)
{
Console.WriteLine($"Hello from plugin! Arg: {arg}");
}
}
""";

//parse using roslyn
var tree = CSharpSyntaxTree.ParseText(
source,
new CSharpParseOptions(
LanguageVersion.Latest,
DocumentationMode.None,
SourceCodeKind.Regular),
cancellationToken: CancellationToken.None);

//compile using roslyn
using var peStream = new MemoryStream();
var pluginAssemblyName = "PluginAssembly";
var emitResult = CSharpCompilation.Create(
assemblyName: pluginAssemblyName,
new SyntaxTree[] { tree },
[
MetadataReference.CreateFromFile(typeof(IPlugin).Assembly.Location),
..Basic.Reference.Assemblies.NetStandard20.References.All
],
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithOverflowChecks(true)
.WithOptimizationLevel(Microsoft.CodeAnalysis.OptimizationLevel.Release))
.Emit(peStream);

if(!emitResult.Success)
{
foreach(var diagnostic in emitResult.Diagnostics)
{
Console.WriteLine(diagnostic);
}

return;
}
//retrieve plugin
var pluginAssemblyBytes = peStream.ToArray();
var pluginAssembly = Assembly.Load(pluginAssemblyBytes);
var pluginType = pluginAssembly.GetTypes().Single(t => t.IsAssignableTo(typeof(IPlugin)));

//set up runtime
using var host = new IsolatedRuntimeHost()
.WithBinDirectoryAssemblyLoader()
.WithAssemblyLoader(s => s == pluginAssemblyName ? pluginAssemblyBytes : null)
.WithWasiConfiguration(new WasiConfiguration()
.WithInheritedStandardOutput());
using var runtime = new IsolatedRuntime(host);

//execute plugin
var arg = "Arg from outside the sandbox.";
runtime.CreateObject(
assemblyName: pluginAssemblyName,
@namespace: pluginType.Namespace,
className: pluginType.Name)
.InvokeVoid(nameof(IPlugin.Execute), arg);
//retrieve plugin
var pluginAssemblyBytes = peStream.ToArray();
var pluginAssembly = Assembly.Load(pluginAssemblyBytes);
var pluginType = pluginAssembly.GetTypes().Single(t => t.IsAssignableTo(typeof(IPlugin)));

//set up runtime
using var host = new IsolatedRuntimeHost()
.WithBinDirectoryAssemblyLoader()
.WithAssemblyLoader(s => s == pluginAssemblyName ? pluginAssemblyBytes : null)
.WithWasiConfiguration(new WasiConfiguration()
.WithInheritedStandardOutput());
using var runtime = new IsolatedRuntime(host);

//execute plugin
var arg = "Arg from outside the sandbox.";
runtime.CreateObject(
assemblyName: pluginAssemblyName,
@namespace: pluginType.Namespace,
className: pluginType.Name)
.InvokeVoid(nameof(IPlugin.Execute), arg);
namespace ConsoleApp1;
using System;

public interface IPlugin
{
void Execute(String arg);
}
namespace ConsoleApp1;
using System;

public interface IPlugin
{
void Execute(String arg);
}
<PackageReference Include="Basic.Reference.Assemblies" Version="1.7.4" />
<PackageReference Include="DotNetIsolator" Version="0.1.0-preview.10032" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
<PackageReference Include="Basic.Reference.Assemblies" Version="1.7.4" />
<PackageReference Include="DotNetIsolator" Version="0.1.0-preview.10032" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
Here's an MVP to get you started. I hope this helps. First, we create our source code. This could be a user supplied source or from whereever. Next, we compile to an in-memory stream using roslyn. Then, we load the fresh assembly and get the relevant plugin type. You could do this using a marker attribute, for example. Afterwards, we set up the WASM isolation and in particular grant it access to the standard output of the host app (for example). Lastly, we invoke the plugin entry method and provide it with an argument. The plugin interface is defined in the same project as the executing code but we could move it to a dedicated library as well. I'm listing the required packages at the end there also. If you have any questions, shoot
Mazranel
MazranelOP4mo ago
I think I'll experiment with this before I implement it into my engine, thanks for the help! Would you recommend moonsharp over NLua? NLua is giving my quite some trouble just getting basic things to work properly And it's got like no documentation either
Rettoph
Rettoph4mo ago
ive not worked with either enough to have an informed opinion. I did like working with mmoonsharp though
Mazranel
MazranelOP4mo ago
I've just tried MoonSharp and it's already seeming to work better, though I'll still need to test it Sorry if this is a dumb question, but do you know how I could dynamically register methods? I'd like to be able to give users access to Raylib functions I read the documentation (https://www.moonsharp.org/callback.html), but this just seems like I'd have to hard code each and every function
Want results from more Discord servers?
Add your server